[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: [\"https://paypal.me/GregMenounos\"]\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- Backup*.rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n/GitHub\n/kw1281test.csproj\n\n.DS_Store\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": []\n}"
  },
  {
    "path": "BlockTitle.cs",
    "content": "﻿namespace BitFab.KW1281Test\n{\n    public enum BlockTitle : byte\n    {\n        ReadIdent = 0x00,\n        ReadRam = 0x01,\n        GroupReadResponseWithText = 0x02,\n        ReadRomEeprom = 0x03,\n        ActuatorTest = 0x04,\n        FaultCodesDelete = 0x05,\n        End = 0x06, // end output, end of communication\n        FaultCodesRead = 0x07, // get errors, all errors output\n        ACK = 0x09,\n        NAK = 0x0A,\n        SoftwareCoding = 0x10,\n        BasicSettingRawDataRead = 0x11,\n        RawDataRead = 0x12,\n        ReadEeprom = 0x19,\n        WriteEeprom = 0x1A,\n        Custom = 0x1B,\n        AdaptationRead = 0x21,\n        AdaptationTest = 0x22,\n        BasicSettingRead = 0x28,\n        GroupRead = 0x29,\n        AdaptationSave = 0x2A,\n        Login = 0x2B,\n        SecurityAccessMode2 = 0x3D,\n        SecurityImmoAccess1 = 0x5C,\n        SecurityAccessMode1 = 0xD7,\n        AdaptationResponse = 0xE6,\n        GroupReadResponse = 0xE7,\n        ReadEepromResponse = 0xEF,\n        RawDataReadResponse = 0xF4,\n        ActuatorTestResponse = 0xF5,\n        AsciiData = 0xF6,\n        WriteEepromResponse = 0xF9,\n        FaultCodesResponse = 0xFC,\n        ReadRomEepromResponse = 0xFD,\n        ReadRamResponse = 0xFE,\n    }\n\n    // http://nefariousmotorsports.com/forum/index.php?topic=8274.0\n    // Block Title - Answer\n    // $00 ECU identification read - $F6\n    // $01 RAM cells read - $FE\n    // $02 RAM cells write - $09\n    // $03 ROM/EPROM/EEPROM read - $FD\n    // $04 Actuator test - $F5\n    // $05 Fault codes delete -$FC\n    // $06 Output end\n    // $07 Fault codes read - $FC\n    // $08 ADC channel read - $FB\n    // $09 Acknowledge\n    // $0A NoAck\n    // $0C EPROM/EEPROM write - $F9\n    // $10 Parameter coding - $F6\n    // $11 Basic setting read - $F4\n    // $12 Measuring values read - $F4\n    // $19 EEPROM (serial) read - $EF\n    // $1A EEPROM (serial) write - $F9\n    // $1B Custom usage \n    // $21 Adaption read - $E6\n    // $22 Adaption transfer - $E6\n    // $27 Start download routine - $E8\n    // $28 Basic setting normed read - $E7\n    // $29 Measuring values normed read - $E7\n    // $2A Adaption save - $E6\n    // $2B Login request - $09/$FD\n    // $3D Security access mode 2 - $09/$D7\n    // $D7 Security access mode 1 - $3D\n}\n"
  },
  {
    "path": "Blocks/AckBlock.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class AckBlock : Block\n    {\n        public AckBlock(List<byte> bytes) : base(bytes)\n        {\n            // Dump();\n        }\n\n        private void Dump()\n        {\n            Log.WriteLine(\"Received ACK block\");\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/ActuatorTestResponseBlock.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class ActuatorTestResponseBlock : Block\n    {\n        public ActuatorTestResponseBlock(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        public string ActuatorName\n        {\n            get\n            {\n                var id = Utils.Dump(Body).Trim();\n                if (_idToName.TryGetValue(id, out string? name))\n                {\n                    return name!;\n                }\n                return id;\n            }\n        }\n\n        private void Dump()\n        {\n            Log.Write(\"Received \\\"Actuator Test Response\\\" block:\");\n            foreach (var b in Body)\n            {\n                Log.Write($\" {b:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n\n        private static readonly Dictionary<string, string> _idToName = new()\n        {\n            { \"02 96\", \"Tachometer\" },\n            { \"02 95\", \"Coolant Temp Gauge\" },\n            { \"02 98\", \"Fuel Gauge\" },\n            { \"02 97\", \"Speedometer\" },\n            { \"03 1E\", \"Segment Test\" },\n            { \"02 72\", \"Glow Plug Warning\" },\n            { \"02 F2\", \"Coolant Temp Warning\" },\n            { \"02 F3\", \"Oil Pressure Warning\" },\n            { \"01 F5\", \"Oil Level Warning\" },\n            { \"04 16\", \"Brake Pad Warning\" },\n            { \"04 3B\", \"Low Washer Fluid Warning\" },\n            { \"04 3A\", \"Low Fuel Warning\" },\n            { \"01 F6\", \"Immobilizer Warning\" },\n            { \"04 17\", \"Brake Warning\" },\n            { \"02 99\", \"Seatbelt Warning\" },\n            { \"02 9A\", \"Gong\" },\n            { \"03 FF\", \"Chime\" },\n            { \"04 AB\", \"End\" },\n        };\n    }\n}\n"
  },
  {
    "path": "Blocks/AdaptationResponseBlock.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class AdaptationResponseBlock : Block\n    {\n        public AdaptationResponseBlock(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        public byte ChannelNumber => Body[0];\n\n        public ushort ChannelValue => (ushort)(Body[1] * 256 + Body[2]);\n\n        private void Dump()\n        {\n            Log.Write(\"Received \\\"Adaptation Response\\\" block:\");\n            foreach (var b in Body)\n            {\n                Log.Write($\" {b:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/AsciiDataBlock.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class AsciiDataBlock : Block\n    {\n        public AsciiDataBlock(List<byte> bytes) : base(bytes)\n        {\n            // Dump();\n        }\n\n        public bool MoreDataAvailable => Bytes[3] > 0x7F;\n\n        public override string ToString()\n        {\n            var sb = new StringBuilder();\n            foreach (var b in Body)\n            {\n                sb.Append((char)(b & 0x7F));\n            }\n            return sb.ToString();\n        }\n\n        private void Dump()\n        {\n            Log.Write($\"Received Ascii data block: \\\"{ToString()}\\\"\");\n\n            if (MoreDataAvailable)\n            {\n                Log.Write(\" (More data available via ReadIdent)\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/Block.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    /// <summary>\n    /// KWP1281 block\n    /// </summary>\n    class Block\n    {\n        public Block(List<byte> bytes)\n        {\n            Bytes = bytes;\n        }\n\n        /// <summary>\n        /// Returns the entire raw block bytes.\n        /// </summary>\n        public List<byte> Bytes { get; }\n\n        public byte Title => Bytes[2];\n\n        /// <summary>\n        /// Returns the body of the block, excluding the length, counter, title and end bytes.\n        /// </summary>\n        public List<byte> Body => Bytes.Skip(3).Take(Bytes.Count - 4).ToList();\n\n        public bool IsAck => Title == (byte)BlockTitle.ACK;\n\n        public bool IsNak => Title == (byte)BlockTitle.NAK;\n\n        public bool IsAckNak => IsAck || IsNak;\n    }\n}\n"
  },
  {
    "path": "Blocks/CodingWscBlock.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class CodingWscBlock : Block\n    {\n        public CodingWscBlock(List<byte> bytes) : base(bytes)\n        {\n            var data = bytes.Skip(4).ToList();\n\n            SoftwareCoding = (data[0] * 256 + data[1]) / 2;\n            WorkshopCode = data[2] * 256 + data[3];\n\n            // Workshop codes > 65535 overflow into the low bit of the software coding\n            if ((data[1] & 1) == 1)\n            {\n                WorkshopCode += 65536;\n            }\n        }\n\n        public override string ToString()\n        {\n            return $\"Software Coding {SoftwareCoding:d5}, Workshop Code: {WorkshopCode:d5}\";\n        }\n\n        public int SoftwareCoding { get; }\n\n        public int WorkshopCode { get; }\n    }\n}\n"
  },
  {
    "path": "Blocks/CustomBlock.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class CustomBlock : Block\n    {\n        public CustomBlock(List<byte> bytes) : base(bytes)\n        {\n            // Dump();\n        }\n\n        private void Dump()\n        {\n            Log.Write(\"Received Custom block:\");\n            for (var i = 3; i < Bytes.Count - 1; i++)\n            {\n                Log.Write($\" {Bytes[i]:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}"
  },
  {
    "path": "Blocks/FaultCodesBlock.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class FaultCodesBlock : Block\n    {\n        public FaultCodesBlock(List<byte> bytes) : base(bytes)\n        {\n            FaultCodes = new();\n\n            IEnumerable<byte> data = Body;\n\n            while (true)\n            {\n                var code = data.Take(3).ToArray();\n                if (code.Length == 0)\n                {\n                    break;\n                }\n\n                var dtc = code[0] * 256 + code[1];\n                var status = code[2];\n\n                var faultCode = new FaultCode(dtc, status);\n                if (!faultCode.Equals(FaultCode.None))\n                {\n                    FaultCodes.Add(faultCode);\n                }\n\n                data = data.Skip(3);\n            }\n        }\n\n        public List<FaultCode> FaultCodes { get; }\n    }\n\n    internal struct FaultCode\n    {\n        public FaultCode(int dtc, int status)\n        {\n            Dtc = dtc;\n            Status = status;\n        }\n\n        public override string ToString()\n        {\n            var status1 = Status & 0x7F;\n            var status2 = (Status >> 7) * 10;\n            return $\"{Dtc:d5} - {status1:d2}-{status2:d2}\";\n        }\n\n        public int Dtc { get; }\n\n        public int Status { get; }\n\n        public static readonly FaultCode None = new FaultCode(0xFFFF, 0x88);\n    }\n}\n"
  },
  {
    "path": "Blocks/GroupReadResponseBlock.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class GroupReadResponseBlock : Block\n    {\n        public GroupReadResponseBlock(List<byte> bytes) : base(bytes)\n        {\n            SensorValues = new List<SensorValue>();\n\n            var bodyBytes = new List<byte>(Body);\n            while (bodyBytes.Count > 2)\n            {\n                var valueBytes = bodyBytes.Take(3).ToArray();\n                SensorValues.Add(\n                    new SensorValue(valueBytes[0], valueBytes[1], valueBytes[2]));\n                bodyBytes = bodyBytes.Skip(3).ToList();\n            }\n\n            if (bodyBytes.Count > 0)\n            {\n                throw new InvalidOperationException(\n                    $\"{nameof(GroupReadResponseBlock)} body ({Utils.DumpBytes(Body)}) should be a multiple of 3 bytes long.\");\n            }\n        }\n\n        public List<SensorValue> SensorValues { get; }\n\n        public override string ToString()\n        {\n            var sb = new StringBuilder();\n\n            foreach(var sensorValue in SensorValues)\n            {\n                if (sb.Length > 0)\n                {\n                    sb.Append(\" | \");\n                }\n                sb.Append(sensorValue.ToString());\n            }\n\n            return sb.ToString();\n        }\n    }\n}\n "
  },
  {
    "path": "Blocks/GroupReadResponseWithTextBlock.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class GroupReadResponseWithTextBlock : Block\n    {\n        public GroupReadResponseWithTextBlock(List<byte> bytes)\n            : base(bytes)\n        {\n            var bodyBytes = new List<byte>(Body);\n            while (bodyBytes.Count > 2)\n            {\n                var subBlockHeader = bodyBytes.Take(3).ToArray();\n                bodyBytes = bodyBytes.Skip(3).ToList();\n\n                int subBlockBodyLength = subBlockHeader[2];\n                if (bodyBytes.Count < subBlockBodyLength)\n                {\n                    throw new InvalidOperationException(\n                        $\"{nameof(GroupReadResponseWithTextBlock)} body ({Utils.DumpBytes(Body)}) contains extra bytes after sub-blocks.\");\n                }\n\n                var subBlock = new SubBlock\n                {\n                    BlockType = subBlockHeader[0],\n                    Data = subBlockHeader[1],\n                    Body = bodyBytes.Take(subBlockBodyLength).ToArray()\n                };\n                bodyBytes = bodyBytes.Skip(subBlockBodyLength).ToList();\n\n                SubBlocks.Add(subBlock);\n                \n                if (subBlock.BlockType == 0x8D)\n                {\n                    var text = Encoding.ASCII.GetString(subBlock.Body, 0, subBlock.Body.Length);\n                    _text = text.Split((char)0x03).ToList();\n                }\n            }\n\n            if (bodyBytes.Count > 0)\n            {\n                throw new InvalidOperationException(\n                    $\"{nameof(GroupReadResponseWithTextBlock)} body ({Utils.DumpBytes(Body)}) contains extra bytes after sub-blocks.\");\n            }\n        }\n\n        private readonly List<string> _text = new();\n\n        public string GetText(int i)\n        {\n            if (i >= 0 && i < _text.Count)\n            {\n                return $\"\\\"{_text[i]}\\\"\";\n            }\n\n            return i.ToString();\n        }\n\n        public override string ToString()\n        {\n            var sb = new StringBuilder();\n\n            foreach (var subBlock in SubBlocks)\n            {\n                sb.Append(subBlock.ToString());\n            }\n\n            return sb.ToString();\n        }\n\n        readonly List<SubBlock> SubBlocks = new();\n\n        class SubBlock\n        {\n            public byte BlockType { get; init; }\n\n            public byte Data { get; init; }\n\n            public byte[] Body { get; init; } = Array.Empty<byte>();\n\n            public override string ToString()\n            {\n                switch(BlockType)\n                {\n                    case 0x8D:\n                        return $\"(${BlockType:X2} ${Data:X2} {Encoding.ASCII.GetString(Body, 0, Body.Length).Replace((char)0x03, '|')})\";\n\n                    default:\n                        return $\"(${BlockType:X2} ${Data:X2}{Utils.Dump(Body)})\";\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/NakBlock.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    class NakBlock : Block\n    {\n        public NakBlock(List<byte> bytes) : base(bytes)\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/RawDataReadResponseBlock.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class RawDataReadResponseBlock : Block\n    {\n        public RawDataReadResponseBlock(List<byte> bytes) : base(bytes)\n        {\n        }\n\n        public override string ToString()\n        {\n            return $\"Raw Data:{Utils.DumpDecimal(Body)}\";\n        }\n    }\n}"
  },
  {
    "path": "Blocks/ReadEepromResponseBlock.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class ReadEepromResponseBlock : Block\n    {\n        public ReadEepromResponseBlock(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        private void Dump()\n        {\n            Log.Write(\"Received \\\"Read EEPROM Response\\\" block:\");\n            foreach (var b in Body)\n            {\n                Log.Write($\" {b:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}"
  },
  {
    "path": "Blocks/ReadRomEepromResponse.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class ReadRomEepromResponse : Block\n    {\n        public ReadRomEepromResponse(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        private void Dump()\n        {\n            Log.Write(\"Received \\\"Read ROM/EEPROM Response\\\" block:\");\n            foreach (var b in Body)\n            {\n                Log.Write($\" {b:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/SecurityAccessMode2Block.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class SecurityAccessMode2Block : Block\n    {\n        public SecurityAccessMode2Block(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        private void Dump()\n        {\n            Log.Write(\"Received \\\"Security Access Mode 2\\\" block:\");\n            foreach (var b in Body)\n            {\n                Log.Write($\" ${b:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/SensorValue.cs",
    "content": "﻿using System;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    public class SensorValue\n    {\n        public byte SensorID { get; }\n\n        public byte A { get; }\n\n        public byte B { get; }\n\n        public SensorValue(byte sensorID, byte a, byte b)\n        {\n            SensorID = sensorID;\n            A = a;\n            B = b;\n        }\n\n        public override string ToString()\n        {\n            // https://www.blafusel.de/obd/obd2_kw1281.html#7\n            return SensorID switch\n            {\n                1 => $\"{0.2 * A * B} rpm\",\n                2 => $\"{(A * 0.002 * B):F1} %\",\n                3 => $\"{(0.002 * A * B):F1} Deg\",\n                4 => $\"{(Math.Abs(B - 127) * 0.01 * A):F1} \\u00B0{(B > 127 ? \"ATDC\" : \"BTDC\")}\", // Degrees\n                5 => $\"{(A * (B-100) * 0.1):F1} \\u00B0C\", // Degrees C\n                6 => $\"{(0.001 * A * B):F1} V\",\n                7 => $\"{0.01 * A * B} km/h\",\n                8 => $\"{(0.1 * A * B):F1}\",\n                9 => $\"{((B - 127) * 0.02 * A):F1} Deg\",\n                10 => $\"{(B == 0 ? \"Cold\" : \"Warm\")}\",\n                11 => $\"{(0.0001 * A * (B - 128) + 1):F1}\",\n                12 => $\"{(0.001 * A * B):F1} \\u2126\", // Ohm\n                13 => $\"{((B - 127) * 0.001 * A):F1} mm\",\n                14 => $\"{(0.005 * A * B):F1} bar\",\n                15 => $\"{(0.01 * A * B):F1} ms\",\n                16 => $\"{Convert.ToString(A, 2).PadLeft(8, '0')} {Convert.ToString(B, 2).PadLeft(8, '0')}\",\n                17 => $\"\\\"{(char)A}{(char)B}\\\"\",\n                18 => $\"{(0.04 * A * B):F1} mbar\",\n                19 => $\"{(A * B * 0.01):F1} l\",\n                20 => $\"{(A * (B - 128) / 128.0):F1} %\",\n                21 => $\"{(0.001 * A * B):F1} V\",\n                22 => $\"{(0.001 * A * B):F1} ms\",\n                23 => $\"{(B / 256.0 * A):F1} %\",\n                24 => $\"{(0.001 * A * B):F1} A\",\n                25 => $\"{((B * 1.421) + (A / 182.0)):F1} g/s\",\n                26 => $\"{B - A} C\",\n                27 => $\"{(Math.Abs(B - 128) * 0.01 * A):F1} \\u00B0{(B < 128 ? \"ATDC\" : \"BTDC\")}\", // Degrees\n                28 => $\"{B - A}\",\n                29 => $\"{(B<A ? \"Map 1\" : \"Map 2\")}\",\n                30 => $\"{(B / 12.0 * A):F1} Deg k/w\",\n                31 => $\"{(B / 2560.0 * A):F1}\",\n                32 => $\"{(B>128 ? B-256 : B)}\",\n                33 => $\"{(A == 0 ? 100.0*B : 100.0*B/A):F1} %\",\n                34 => $\"{((B - 128) * 0.01 * A):F1} kW\",\n                35 => $\"{(0.01 * A * B):F1} l/h\",\n                36 => $\"{A * 2560 + B * 10} km\",\n                // 37 => ???,\n                38 => $\"{((B - 128) * 0.001 * A):F1} Deg k/w\",\n                39 => $\"{(B/256.0*A):F1} mg/h\",\n                40 => $\"{(B * 0.1 + (25.5 * A) - 400):F1} A\",\n                41 => $\"{(B + A * 255)} Ah\",\n                42 => $\"{(B * 0.1 + (25.5 * A) - 400):F1} Kw\",\n                43 => $\"{(B * 0.1 + (25.5 * A)):F1} V\",\n                44 => $\"{A:D2}:{B:D2}\",\n                45 => $\"{(0.1 * A * B / 100.0):F1}\",\n                46 => $\"{((A * B - 3200) * 0.0027):F1} Deg k/w\",\n                47 => $\"{((B - 128) * A)} ms\",\n                48 => $\"{B + A * 255}\",\n                49 => $\"{(B / 4.0 * A * 0.1):F1} mg/h\",\n                50 => $\"{(A == 0 ? (B - 128) / 0.01 : (B - 128) / (0.01 * A)):F1} mbar\",\n                51 => $\"{(((B - 128) / 255.0) * A):F1} mg/h\",\n                52 => $\"{(B * 0.02 * A - A):F1} Nm\",\n                53 => $\"{((B - 128) * 1.4222 + 0.006 * A):F1} g/s\",\n                54 => $\"{A * 256 + B}\",\n                55 => $\"{(A * B / 200.0):F1} s\",\n                56 => $\"{A * 256 + B} WSC\",\n                57 => $\"{A * 256 + B + 65536} WSC\",\n                58 => $\"{(B > 128 ? 1.0225 * (256 - B) : 1.0225 * B):F1} /s\",\n                59 => $\"{((A * 256 + B) / 32768.0):F1}\",\n                60 => $\"{((A * 256 + B) * 0.01):F1} sec\",\n                61 => $\"{(A==0 ? (B - 128) : (B - 128) / A):F1}\",\n                62 => $\"{(0.256 * A * B):F1} S\",\n                63 => $\"\\\"{(char)A}{(char)B}\\\"?\",\n                64 => $\"{A+B} \\u2126\", // Ohm\n                65 => $\"{(0.01 * A * (B - 127)):F1} mm\",\n                66 => $\"{((A * B) / 511.12):F1} V\",\n                67 => $\"{((640 * A) + B * 2.5):F1} Deg\",\n                68 => $\"{((256 * A + B) / 7.365):F1} deg/s\",\n                69 => $\"{((256 * A + B) * 0.3254):F1} Bar\",\n                70 => $\"{((256 * A + B) * 0.192):F1} m/s\\u00B2\", // squared\n                _ => $\"({SensorID} {A} {B})\",\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "Blocks/UnknownBlock.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class UnknownBlock : Block\n    {\n        public UnknownBlock(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        private void Dump()\n        {\n            Log.Write($\"Received ${Title:X2} block:\");\n            foreach (var b in Bytes)\n            {\n                Log.Write($\" 0x{b:X2}\");\n            }\n            Log.WriteLine();\n        }\n    }\n}"
  },
  {
    "path": "Blocks/WriteEepromResponseBlock.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\n\nnamespace BitFab.KW1281Test.Blocks\n{\n    internal class WriteEepromResponseBlock : Block\n    {\n        public WriteEepromResponseBlock(List<byte> bytes) : base(bytes)\n        {\n            Dump();\n        }\n\n        private void Dump()\n        {\n            Log.Write(\"Received \\\"Write EEPROM Response\\\" block:\");\n            foreach (var b in Body)\n            {\n                Log.Write($\" {b:X2}\");\n            }\n\n            Log.WriteLine();\n        }\n    }\n}"
  },
  {
    "path": "BusyWait.cs",
    "content": "using System.Diagnostics;\n\nnamespace BitFab.KW1281Test;\n\npublic class BusyWait\n{\n    private readonly long _ticksPerCycle;\n    private long? _nextTickTimestamp;\n\n    public BusyWait(long msPerCycle)\n    {\n        _ticksPerCycle = msPerCycle * TicksPerMs;\n    }\n\n    public void DelayUntilNextCycle()\n    {\n        _nextTickTimestamp ??= Stopwatch.GetTimestamp() + _ticksPerCycle;\n\n        while (Stopwatch.GetTimestamp() < _nextTickTimestamp)\n        {\n        }\n        _nextTickTimestamp += _ticksPerCycle;\n    }\n\n    public static void Delay(long ms)\n    {\n        var waiter = new BusyWait(ms);\n        waiter.DelayUntilNextCycle();\n    }\n\n    private static readonly long TicksPerMs = Stopwatch.Frequency / 1000;\n}"
  },
  {
    "path": "Cluster/AudiC5Cluster.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.IO.Ports;\nusing System.Linq;\nusing System.Text;\nusing System.Threading;\nusing BitFab.KW1281Test.Blocks;\n\nnamespace BitFab.KW1281Test.Cluster;\n\ninternal class AudiC5Cluster : ICluster\n{\n    public void UnlockForEepromReadWrite()\n    {\n        string[] passwords =\n        [\n            \"loginas9\",\n            \"n7KB2Qat\",\n        ];\n\n        var succeeded = false;\n        foreach (var password in passwords)\n        {\n            Log.WriteLine(\"Sending custom login block\");\n            var blockBytes = new List<byte>([0x1B, 0x80]); // Custom 0x80\n            blockBytes.AddRange(Encoding.ASCII.GetBytes(password));\n            _kw1281Dialog.SendBlock(blockBytes);\n\n            var block = _kw1281Dialog.ReceiveBlock();\n            if (block is NakBlock)\n            {\n                continue;\n            }\n            else if (block is not AckBlock)\n            {\n                throw new InvalidOperationException(\n                    $\"Expected ACK block but received: {block}\");\n            }\n\n            succeeded = true;\n        }\n\n        if (!succeeded)\n        {\n            throw new InvalidOperationException(\"Unable to login to cluster\");\n        }\n\n        var @interface = _kw1281Dialog.KwpCommon.Interface;\n        @interface.SetBaudRate(19200);\n        @interface.SetParity(Parity.Even);\n        @interface.ClearReceiveBuffer();\n\n        Thread.Sleep(TimeSpan.FromSeconds(2));\n    }\n\n    public string DumpEeprom(uint? address, uint? length, string? dumpFileName)\n    {\n        ArgumentNullException.ThrowIfNull(address);\n        ArgumentNullException.ThrowIfNull(length);\n        ArgumentNullException.ThrowIfNull(dumpFileName);\n\n        WriteBlock([Constants.Hello]);\n\n        var blockBytes = ReadBlock();\n        Log.WriteLine($\"Received block:{Utils.Dump(blockBytes)}\");\n        if (BlockTitle(blockBytes) != Constants.Hello)\n        {\n            Log.WriteLine($\"Warning: Expected block of type ${Constants.Hello:X2}\");\n        }\n\n        string[] passwords =\n        [\n            \"19xDR8xS\",\n            \"vdokombi\",\n            \"w10kombi\",\n            \"w10serie\",\n        ];\n\n        var succeeded = false;\n        foreach (var password in passwords)\n        {\n            Log.WriteLine(\"Sending login request\");\n\n            blockBytes = [Constants.Login, 0x9D];\n            blockBytes.AddRange(Encoding.ASCII.GetBytes(password));\n            WriteBlock(blockBytes);\n\n            blockBytes = ReadBlock();\n            Log.WriteLine($\"Received block:{Utils.Dump(blockBytes)}\");\n\n            if (BlockTitle(blockBytes) == Constants.Ack)\n            {\n                succeeded = true;\n                break;\n            }\n            else\n            {\n                Log.WriteLine($\"Warning: Expected block of type ${Constants.Ack:X2}\");\n            }\n        }\n\n        if (!succeeded)\n        {\n            throw new InvalidOperationException(\"Unable to login to cluster\");\n        }\n        else\n        {\n            Log.WriteLine(\"Succeeded\");\n        }\n\n        Log.WriteLine($\"Dumping EEPROM to {dumpFileName}\");\n        DumpEeprom(address.Value, length.Value, maxReadLength: 0x10, dumpFileName);\n\n        _kw1281Dialog.SetDisconnected();\n\n        return dumpFileName;\n    }\n\n    private void DumpEeprom(\n        uint startAddr, uint length, byte maxReadLength, string fileName)\n    {\n        using var fs = File.Create(fileName, bufferSize: maxReadLength, FileOptions.WriteThrough);\n\n        var succeeded = true;\n        for (var addr = startAddr; addr < startAddr + length; addr += maxReadLength)\n        {\n            var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);\n            var blockBytes = ReadEepromByAddress(addr, readLength);\n\n            if (blockBytes.Count != readLength)\n            {\n                succeeded = false;\n                blockBytes.AddRange(\n                    Enumerable.Repeat((byte)0, readLength - blockBytes.Count));\n            }\n\n            fs.Write(blockBytes.ToArray(), offset: 0, blockBytes.Count);\n            fs.Flush();\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    private List<byte> ReadEepromByAddress(uint addr, byte readLength)\n    {\n        List<byte> blockBytes =\n        [\n            Constants.ReadEeprom,\n            readLength,\n            (byte)(addr >> 8),\n            (byte)(addr & 0xFF)\n        ];\n        WriteBlock(blockBytes);\n\n        blockBytes = ReadBlock();\n        Log.WriteLine($\"Received block:{Utils.Dump(blockBytes)}\");\n\n        if (BlockTitle(blockBytes) != Constants.ReadEeprom)\n        {\n            throw new InvalidOperationException($\"Expected block of type ${Constants.ReadEeprom:X2}\");\n        }\n\n        var expectedLength = readLength + 4;\n        var actualLength = blockBytes.Count;\n        if (blockBytes.Count != expectedLength)\n        {\n            Log.WriteLine(\n        $\"Warning: Expected block length ${expectedLength:X2} but length is ${actualLength:X2}\");\n        }\n\n        return blockBytes.Skip(3).Take(actualLength - 4).ToList();\n    }\n\n    private static byte BlockTitle(IReadOnlyList<byte> blockBytes)\n    {\n        return blockBytes[2];\n    }\n\n    private void WriteBlock(IReadOnlyCollection<byte> bodyBytes)\n    {\n        byte checksum = 0x00;\n\n        WriteBlockByte(Constants.StartOfBlock);\n        WriteBlockByte((byte)(bodyBytes.Count + 3)); // Block length\n        foreach (var bodyByte in bodyBytes)\n        {\n            WriteBlockByte(bodyByte);\n        }\n\n        _kw1281Dialog.KwpCommon.WriteByte(checksum);\n        return;\n\n        void WriteBlockByte(byte b)\n        {\n            _kw1281Dialog.KwpCommon.WriteByte(b);\n            checksum ^= b;\n        }\n    }\n\n    private List<byte> ReadBlock()\n    {\n        var blockBytes = new List<byte>();\n        byte checksum = 0x00;\n\n        try\n        {\n            var header = ReadByte();\n            var blockSize = ReadByte();\n            for (var i = 0; i < blockSize - 2; i++)\n            {\n                ReadByte();\n            }\n\n            if (header != Constants.StartOfBlock)\n            {\n                throw new InvalidOperationException($\"Expected $D1 header byte but got ${header:X2}\");\n            }\n\n            if (checksum != 0x00)\n            {\n                throw new InvalidOperationException($\"Expected $00 block checksum but got ${checksum:X2}\");\n            }\n        }\n        catch (Exception e)\n        {\n            Log.WriteLine($\"Error reading block: {e}\");\n            Log.WriteLine($\"Partial block: {Utils.Dump(blockBytes)}\");\n            throw;\n        }\n\n        return blockBytes;\n\n        byte ReadByte()\n        {\n            var b = _kw1281Dialog.KwpCommon.ReadByte();\n            checksum ^= b;\n            blockBytes.Add(b);\n            return b;\n        }\n    }\n\n    private static class Constants\n    {\n        public const byte StartOfBlock = 0xD1;\n\n        public const byte Ack = 0x06;\n        public const byte Nak = 0x15;\n        public const byte Hello = 0x49;\n        public const byte Login = 0x53;\n        public const byte ReadEeprom = 0x72;\n    }\n\n    private readonly IKW1281Dialog _kw1281Dialog;\n\n    public AudiC5Cluster(IKW1281Dialog kw1281Dialog)\n    {\n        _kw1281Dialog = kw1281Dialog;\n    }\n}"
  },
  {
    "path": "Cluster/BoschRBxCluster.cs",
    "content": "﻿using BitFab.KW1281Test.Kwp2000;\nusing System;\nusing System.Linq;\nusing System.Threading;\nusing Service = BitFab.KW1281Test.Kwp2000.DiagnosticService;\n\nnamespace BitFab.KW1281Test.Cluster;\n\nclass BoschRBxCluster : ICluster\n{\n    public void UnlockForEepromReadWrite()\n    {\n        SecurityAccess(0xFB);\n    }\n\n    public string DumpEeprom(\n        uint? optionalAddress, uint? optionalLength, string? optionalFileName)\n    {\n        uint address = optionalAddress ?? 0x10400;\n        uint length = optionalLength ?? 0x400;\n        string filename = optionalFileName ?? $\"RBx_0x{address:X6}_mem.bin\";\n\n        _kwp2000.DumpMem(address, length, filename);\n\n        return filename;\n    }\n\n    public bool SecurityAccess(byte accessMode)\n    {\n        const byte identificationOption = 0x94;\n        var responseMsg = _kwp2000.SendReceive(Service.readEcuIdentification, new byte[] { identificationOption });\n        if (responseMsg.Body[0] != identificationOption)\n        {\n            throw new InvalidOperationException($\"Received unexpected identificationOption: {responseMsg.Body[0]:X2}\");\n        }\n        Log.WriteLine(Utils.DumpAscii(responseMsg.Body.Skip(1)));\n\n        const int maxTries = 16;\n        for (var i = 0; i < maxTries; i++)\n        {\n            responseMsg = _kwp2000.SendReceive(Service.securityAccess, [accessMode]);\n            if (responseMsg.Body[0] != accessMode)\n            {\n                throw new InvalidOperationException($\"Received unexpected accessMode: {responseMsg.Body[0]:X2}\");\n            }\n            var seedBytes = responseMsg.Body.Skip(1).ToArray();\n            var seed = (uint)(\n                (seedBytes[0] << 24) |\n                (seedBytes[1] << 16) |\n                (seedBytes[2] << 8) |\n                seedBytes[3]);\n            var key = CalcRBxKey(seed);\n\n            try\n            {\n                responseMsg = _kwp2000.SendReceive(Service.securityAccess,\n                    new[] {\n                        (byte)(accessMode + 1),\n                        (byte)((key >> 24) & 0xFF),\n                        (byte)((key >> 16) & 0xFF),\n                        (byte)((key >> 8) & 0xFF),\n                        (byte)(key & 0xFF)\n                    });\n\n                Log.WriteLine(\"Success!!!\");\n                return true;\n            }\n            catch (NegativeResponseException)\n            {\n                if (i < (maxTries - 1))\n                {\n                    Log.WriteLine(\"Trying again.\");\n                }\n            }\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// Toggle an Audi A4 RB4 cluster between Adapted mode (6) and New mode (4).\n    /// Cluster should already be logged in and unlocked for EEPROM read/write.\n    /// </summary>\n    public void ToggleRB4Mode()\n    {\n        _kwp2000.StartDiagnosticSession(0x84, 0x14);\n\n        Thread.Sleep(350);\n\n        byte[] bytes = _kwp2000.ReadMemoryByAddress(0x010450, 2);\n        if (bytes[0] != (byte)'A' && bytes[1] != (byte)'U')\n        {\n            Log.WriteLine(\"Cluster is not an Audi cluster!\");\n        }\n        else\n        {\n            try\n            {\n                bytes = _kwp2000.ReadMemoryByAddress(0x010000, 0x10);\n                Log.WriteLine(\"Cluster is in New mode (4).\");\n            }\n            catch (NegativeResponseException)\n            {\n                Log.WriteLine(\"Cluster is in Adapted mode (6).\");\n            }\n\n            Log.WriteLine(\"Toggling cluster mode...\");\n\n            foreach (var address in new uint[] { 0x01044F, 0x01052F, 0x01062F })\n            {\n                bytes = _kwp2000.ReadMemoryByAddress(address, 1);\n                bytes[0] ^= 0x12;\n                _kwp2000.WriteMemoryByAddress(address, 1, bytes);\n            }\n        }\n\n        Log.WriteLine(\"Resetting cluster...\");\n\n        _kwp2000.EcuReset(0x01);\n    }\n\n    static uint CalcRBxKey(uint seed)\n    {\n        uint key = 0x03249272 + (seed ^ 0xf8253947);\n        return key;\n    }\n\n    private readonly KW2000Dialog _kwp2000;\n\n    public BoschRBxCluster(KW2000Dialog kwp2000)\n    {\n        _kwp2000 = kwp2000;\n    }\n}\n"
  },
  {
    "path": "Cluster/ICluster.cs",
    "content": "﻿namespace BitFab.KW1281Test.Cluster\n{\n    internal interface ICluster\n    {\n        void UnlockForEepromReadWrite();\n\n        string DumpEeprom(uint? address, uint? length, string? dumpFileName);\n    }\n}\n"
  },
  {
    "path": "Cluster/MarelliCluster.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\n\nnamespace BitFab.KW1281Test.Cluster\n{\n    class MarelliCluster : ICluster\n    {\n        public void UnlockForEepromReadWrite()\n        {\n            // Nothing to do\n        }\n\n        public string DumpEeprom(uint? address, uint? length, string? dumpFileName)\n        {\n            address ??= GetDefaultAddress();\n            dumpFileName ??= $\"marelli_mem_${address:X4}.bin\";\n\n            _ = DumpMem(dumpFileName, (ushort)address, (ushort?)length);\n\n            return dumpFileName;\n        }\n\n        private ushort GetDefaultAddress()\n        {\n            if (HasSmallEeprom())\n            {\n                return 3072; // $0C00\n            }\n            else if (HasLargeEeprom())\n            {\n                return 14336; // $3800\n            }\n            else\n            {\n                Log.WriteLine();\n                Log.WriteLine(\"Unsupported Marelli cluster version.\");\n                Log.WriteLine(\"You can try the following commands to see if either produces a dump file.\");\n                Log.WriteLine(\"Then please contact the program author with the results.\");\n                Log.WriteLine();\n\n                var prefix = string.Join(' ', Program.CommandAndArgs.Take(4));\n                Log.WriteLine($\"{prefix} DumpMarelliMem 3072 1024\");\n                Log.WriteLine($\"{prefix} DumpMarelliMem 14336 2048\");\n\n                throw new UnableToProceedException();\n            }\n        }\n\n        /// <summary>\n        /// Dumps memory from a Marelli cluster to a file.\n        /// </summary>\n        private byte[] DumpMem(\n            string filename,\n            ushort address,\n            ushort? count = null)\n        {\n            byte entryH; // High byte of code entry point\n            byte regBlockH; // High byte of register block\n\n            if (_ecuInfo.Contains(\"M73 D0\"))    // Audi TT\n            {\n                entryH = 0x00; // $0000\n                regBlockH = (byte)((address == 0x3800) ? 0x20 : 0x08);\n                count ??= (ushort)((address == 0x3800) ? 0x800 : 0x400);\n            }\n            else if (HasSmallEeprom())\n            {\n                entryH = 0x02; // $0200\n                regBlockH = 0x08; // $0800\n                count ??= 1024; // $0400\n            }\n            else if (HasLargeEeprom())\n            {\n                entryH = 0x18; // $1800\n                regBlockH = 0x20; // $2000\n                count ??= 2048; // $0800\n            }\n            else if (address == 3072 && count == 1024)\n            {\n                Log.WriteLine(\"Untested cluster version! You may need to disconnect your battery if this fails.\");\n\n                entryH = 0x02;\n                regBlockH = 0x08;\n            }\n            else if (address == 14336 && count == 2048)\n            {\n                Log.WriteLine(\"Untested cluster version! You may need to disconnect your battery if this fails.\");\n\n                entryH = 0x18;\n                regBlockH = 0x20;\n            }\n            else\n            {\n                Log.WriteLine(\"Unsupported cluster software version\");\n                return [];\n            }\n\n            Log.WriteLine($\"entryH: 0x{entryH:X2}, regBlockH: 0x{regBlockH:X2}, count: 0x{count:X4}\");\n\n            Log.WriteLine(\"Sending block 0x6C\");\n            _kwp1281.SendBlock([0x6C]);\n\n            Thread.Sleep(250);\n\n            Log.WriteLine(\"Writing data to cluster microcontroller\");\n            var data = new byte[]\n            {\n                0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x50, 0x34,\n                entryH, 0x00, // Entry point $xx00\n            };\n            if (!WriteMarelliBlockAndReadAck(data))\n            {\n                return [];\n            }\n\n            // Now we write a small memory dump program to the 68HC12 processor\n\n            Log.WriteLine(\"Writing memory dump program to cluster microcontroller\");\n            Log.WriteLine($\"(Entry: ${entryH:X2}00, RegBlock: ${regBlockH:X2}00, Start: ${address:X4}, Count: ${count:X4})\");\n\n            var startH = (byte)(address / 256);\n            var startL = (byte)(address % 256);\n\n            var end = address + count;\n            var endH = (byte)(end / 256);\n            var endL = (byte)(end % 256);\n\n            var program = new byte[]\n            {\n                entryH, 0x00, // Address $xx00\n\n                0x14, 0x50,                     // orcc #$50\n                0x07, 0x32,                     // bsr FeedWatchdog\n\n                // Set baud rate to 9600\n                0xC7,                           // clrb\n                0x7B, regBlockH, 0xC8,          // stab SC1BDH\n                0xC6, 0x34,                     // ldab #$34\n                0x7B, regBlockH, 0xC9,          // stab SC1BDL\n\n                // Enable transmit, disable UART interrupts\n                0xC6, 0x08,                     // ldab #$08\n                0x7B, regBlockH, 0xCB,          // stab SC1CR2\n\n                0xCE, startH, startL,           // ldx #start\n                // SendLoop:\n                0xA6, 0x30,                     // ldaa 1,X+\n                0x07, 0x0F,                     // bsr SendByte\n                0x8E, endH, endL,               // cpx #end\n                0x26, 0xF7,                     // bne SendLoop\n                // Poison the watchdog to force a reboot\n                0xCC, 0x11, 0x11,               // ldd #$1111\n                0x7B, regBlockH, 0x17,          // stab COPRST\n                0x7A, regBlockH, 0x17,          // staa COPRST\n                0x3D,                           // rts\n\n                // SendByte:\n                0xF6, regBlockH, 0xCC,          // ldab SC1SR1\n                0x7A, regBlockH, 0xCF,          // staa SC1DRL\n                // TxBusy:\n                0x07, 0x06,                     // bsr FeedWatchdog\n                // Loop until TC (Transmit Complete) bit is set\n                0x1F, regBlockH, 0xCC, 0x40, 0xF9,   // brclr SC1SR1,$40,TxBusy\n                0x3D,                           // rts\n\n                // FeedWatchdog:\n                0xCC, 0x55, 0xAA,               // ldd #$55AA\n                0x7B, regBlockH, 0x17,          // stab COPRST\n                0x7A, regBlockH, 0x17,          // staa COPRST\n                0x3D,                           // rts\n            };\n            if (!WriteMarelliBlockAndReadAck(program))\n            {\n                return Array.Empty<byte>();\n            }\n\n            Log.WriteLine(\"Receiving memory dump\");\n\n            var kwpCommon = _kwp1281.KwpCommon;\n            var mem = new List<byte>();\n            for (int i = 0; i < count; i++)\n            {\n                var b = kwpCommon.ReadByte();\n                mem.Add(b);\n            }\n\n            File.WriteAllBytes(filename, mem.ToArray());\n            Log.WriteLine($\"Saved memory dump to {filename}\");\n\n            Log.WriteLine(\"Done\");\n\n            _kwp1281.SetDisconnected(); // Don't try to send EndCommunication block\n\n            return mem.ToArray();\n        }\n\n        private bool WriteMarelliBlockAndReadAck(byte[] data)\n        {\n            var kwpCommon = _kwp1281.KwpCommon;\n\n            var count = (ushort)(data.Length + 2); // Count includes 2-byte checksum\n            var countH = (byte)(count / 256);\n            var countL = (byte)(count % 256);\n            kwpCommon.WriteByte(countH);\n            kwpCommon.WriteByte(countL);\n\n            var sum = (ushort)(countH + countL);\n            foreach (var b in data)\n            {\n                kwpCommon.WriteByte(b);\n                sum += b;\n            }\n            kwpCommon.WriteByte((byte)(sum / 256));\n            kwpCommon.WriteByte((byte)(sum % 256));\n\n            var expectedAck = new byte[] { 0x03, 0x09, 0x00, 0x0C };\n\n            Log.WriteLine(\"Receiving ACK\");\n            var ack = new List<byte>();\n            for (int i = 0; i < 4; i++)\n            {\n                var b = kwpCommon.ReadByte();\n                ack.Add(b);\n            }\n            if (!ack.SequenceEqual(expectedAck))\n            {\n                Log.WriteLine($\"Expected ACK but received {Utils.Dump(ack)}\");\n                return false;\n            }\n\n            return true;\n        }\n\n        private readonly string[] _smallEepromEcus =\n        [\n            \"1C0920800\",    // Beetle 1C0920800C M73 V07\n            \"1C0920806\",    // Beetle 1C0920806G M73 V03\n            \"1C0920901\",    // Beetle 1C0920901C M73 V07\n            \"1C0920905\",    // Beetle 1C0920905F M73 V03\n            \"1C0920906\",    // Beetle 1C0920906A M73 V03\n            \"8N1919880E KOMBI+WEGFAHRS. M73 D23\",   // Audi TT\n            \"8N1920930\",    // Audi TT 8N1920930B M73 D23\n        ];\n\n        private bool HasSmallEeprom() => _smallEepromEcus.Any(model => _ecuInfo.Contains(model));\n\n        private readonly string[] _largeEepromEcus =\n        [\n            \"1C0920821\",    // KOMBI+WEGFAHRS. M73 V08 (Beetle 2003)\n            \"1C0920921\",    // Beetle 1C0920921G M73 V08\n            \"1C0920941\",    // Beetle 1C0920941LX M73 V03\n            \"1C0920951\",    // Beetle 1C0920951A M73 V02\n            \"8D0920900R\",   // KOMBI+WEGFAHRS. M73 D54 (Audi A4 B5 2001)\n            \"8L0920900B\",   // KOMBI+WEGFAHRS. M73 D13 (Audi A3 8L 2002, ASZ diesel engine)\n            \"8L0920900E\",   // KOMBI+WEGFAHRS. M73 D56\n            \"8N1919880E KOMBI+WEGFAHRS. M73 D26\",   // Audi TT\n            \"8N1920980\",    // Audi TT 8N1920980E M73 D14\n            \"8N2919910A\",   // KOMBI+WEGFAHRS. M73 D29, Audi TT\n            \"8N2920930\",    // Audi TT 8N2920930C M73 D55\n            \"8N2920980\",    // Audi TT 8N2920980A M73 D14\n        ];\n\n        // 1C0920821 KOMBI+WEGFAHRS. M73 V08\n\n        private bool HasLargeEeprom() => _largeEepromEcus.Any(model => _ecuInfo.Contains(model));\n\n        /// <summary>\n        /// Search for the SKC using the 2 methods described here:\n        /// https://github.com/gmenounos/kw1281test/issues/50#issuecomment-1770255129\n        /// </summary>\n        public static ushort? GetSkc(byte[] buf)\n        {\n            // If the EEPROM contains a 14-digit Immobilizer ID then the SKC should be immediately prior to that\n            var immoIdOffset = FindImmobilizerId(buf);\n            if (immoIdOffset is >= 2)\n            {\n                return Utils.GetShortBE(buf, immoIdOffset.Value-2);\n            }\n\n            // 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\n            var keyCountOffset = FindKeyCount(buf);\n            if (keyCountOffset is >= 2)\n            {\n                return Utils.GetShortBE(buf, keyCountOffset.Value-2);\n            }\n\n            return null;\n        }\n\n        /// <summary>\n        /// Search the buffer for a 14 byte long string of uppercase letters and numbers beginning with VWZ or AUZ\n        /// </summary>\n        private static int? FindImmobilizerId(IReadOnlyList<byte> buf)\n        {\n            for (var i = 0; i < buf.Count - 14; i++)\n            {\n                if (!(buf[i] == 'V' && buf[i + 1] == 'W') &&\n                    !(buf[i] == 'A' && buf[i + 1] == 'U'))\n                {\n                    continue;\n                }\n\n                if (buf[i + 2] != 'Z')\n                {\n                    continue;\n                }\n\n                var isValid = true;\n                for (var j = 3; j < 14; j++)\n                {\n                    var b = buf[i + j];\n                    if (b is >= (byte)'0' and <= (byte)'9' or >= (byte)'A' and <= (byte)'Z')\n                    {\n                        continue;\n                    }\n\n                    isValid = false;\n                    break;\n                }\n\n                if (isValid)\n                {\n                    return i;\n                }\n            }\n\n            return null;\n        }\n\n        /// <summary>\n        /// Search the buffer for the 3 byte sequence 00,01,0F or 00,02,0F or 00,03,0F or 00,04,0F\n        /// (2nd digit is probably the number of keys)\n        /// </summary>\n        private static int? FindKeyCount(IReadOnlyList<byte> buf)\n        {\n            for (var i = 0; i < buf.Count - 3; i++)\n            {\n                if (buf[i] != 0)\n                {\n                    continue;\n                }\n\n                if (buf[i + 1] != 1 && buf[i + 1] != 2 && buf[i + 1] != 3 && buf[i + 1] != 4)\n                {\n                    continue;\n                }\n\n                if (buf[i + 2] != 0x0F)\n                {\n                    continue;\n                }\n\n                return i;\n            }\n\n            return null;\n        }\n\n        private readonly IKW1281Dialog _kwp1281;\n        private readonly string _ecuInfo;\n\n        public MarelliCluster(IKW1281Dialog kwp1281, string ecuInfo)\n        {\n            _kwp1281 = kwp1281;\n            _ecuInfo = ecuInfo;\n        }\n    }\n}\n"
  },
  {
    "path": "Cluster/MotometerBOOCluster.cs",
    "content": "﻿using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Cluster;\n\ninternal class MotometerBOOCluster : ICluster\n{\n    public void UnlockForEepromReadWrite()\n    {\n        string softwareVersion = GetClusterInfo();\n        if (softwareVersion.Length < 10 ||\n            !VersionToLogin.TryGetValue(softwareVersion[..10], out ushort login))\n        {\n            Log.WriteLine(\"Warning: Unknown software version. Login may fail.\");\n            login = 11899;\n        }\n\n        _kwp1281!.Login(login, workshopCode: 0);\n\n        Log.WriteLine($\"Sending Custom $08 $15 block\");\n        if (SendCustom(0x08, 0x15))\n        {\n            return;\n        }\n\n        Log.WriteLine(\"$08 $15 failed. Trying all combinations (this may take a while)...\");\n\n        for (int first = 0; first < 0x100; first++)\n        {\n            Log.WriteLine($\"Trying ${first:X2} $00-$FF\");\n\n            for (int second = 0; second < 0x100; second++)\n            {\n                if (SendCustom(first, second))\n                {\n                    Log.WriteLine($\"Combination ${first:X2} ${second:X2} Succeeded.\");\n                    Log.WriteLine(\"Please report this to the program author.\");\n                    return;\n                }\n            }\n        }\n\n        Log.WriteLine(\"All combinations failed. EEPROM access will likely fail.\");\n    }\n\n    private bool SendCustom(int first, int second)\n    {\n        _kwp1281.SendBlock(new List<byte> { 0x1B, (byte)first, (byte)second });\n        var block = _kwp1281.ReceiveBlocks().FirstOrDefault();\n\n        if (block is NakBlock)\n        {\n            return false;\n        }\n        else if (block is AckBlock)\n        {\n            return true;\n        }\n        else\n        {\n            throw new InvalidOperationException(\n                $\"Expected ACK or NAK block but got: {block}\");\n        }\n    }\n\n    private readonly Dictionary<string, ushort> VersionToLogin = new()\n    {\n        { \"a0prj008.1\", 10164 },\n        { \"A0prj008.2\", 10164 },\n        { \"a4prj010.1\", 21597 },\n        { \"a4prj012.1\", 21597 },\n        { \"h1340_05.2\", 21701 },\n        { \"h1340_06.2\", 21601 },\n        { \"h9340_08.1\", 11899 },\n        { \"h9340_08.2\", 11899 },\n        { \"h9340_09.1\", 11899 },\n        { \"h9340_10.1\", 11899 },\n        { \"h9340_10.2\", 11899 },\n        { \"h9340_10.3\", 11899 },\n        { \"h9340_11.2\", 11899 },\n        { \"se110_05.2\", 13473 },\n        { \"v9119_07.1\", 19126 },\n        { \"v9119_07.3\", 11064 },\n        { \"v9230_03.1\", 10501 },\n        { \"v9230_03.2\", 10501 },\n        { \"v9230_05.1\", 44479 },\n        { \"v9230_05.2\", 44479 },\n        { \"v9230_06.3\", 23775 },\n        { \"v9230_07.2\", 10164 },\n        { \"v9230_08.1\", 10164 },\n        { \"v9230_08.2\", 10164 },\n        { \"vw110_04.2\", 08721 },\n        { \"VW230_06.1\", 47165 },\n        { \"vw340_07.2\", 05555 },\n    };\n\n    public string DumpEeprom(\n        uint? optionalAddress, uint? optionalLength, string? optionalFileName)\n    {\n        uint address = optionalAddress ?? 0;\n        uint length = optionalLength ?? 0x100;\n        string filename = optionalFileName ?? $\"BOOMM0_0x{address:X6}_eeprom.bin\";\n\n#if false\n        var identInfo = _kwp1281.ReadIdent().First().ToString()\n            .Split(Environment.NewLine).First() // Sometimes ReadIdent() can return multiple lines\n            .Replace(' ', '_');\n\n        var dumpFileName = filename ?? $\"{identInfo}_0x{startAddress:X4}_eeprom.bin\";\n        foreach (var c in Path.GetInvalidFileNameChars())\n        {\n            dumpFileName = dumpFileName.Replace(c, 'X');\n        }\n        foreach (var c in Path.GetInvalidPathChars())\n        {\n            dumpFileName = dumpFileName.Replace(c, 'X');\n        }\n\n        Log.WriteLine($\"Saving EEPROM dump to {dumpFileName}\");\n        DumpEeprom(startAddress, length, maxReadLength: 16, dumpFileName);\n        Log.WriteLine($\"Saved EEPROM dump to {dumpFileName}\");\n\n        return dumpFileName;\n#endif\n        throw new NotImplementedException();\n    }\n\n    private string GetClusterInfo()\n    {\n        Log.WriteLine(\"Sending 0x43 block\");\n\n        _kwp1281.SendBlock([0x43]);\n        var blocks = _kwp1281.ReceiveBlocks().Where(b => !b.IsAckNak).ToList();\n        foreach (var block in blocks)\n        {\n            Log.WriteLine($\"{Utils.DumpAscii(block.Body)}\");\n        }\n\n        return Utils.DumpAscii(blocks[0].Body);\n    }\n\n    private readonly IKW1281Dialog _kwp1281;\n\n    public MotometerBOOCluster(IKW1281Dialog kwp1281)\n    {\n        _kwp1281 = kwp1281;\n    }\n}\n"
  },
  {
    "path": "Cluster/VdoCluster.cs",
    "content": "﻿using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace BitFab.KW1281Test.Cluster;\n\ninternal class VdoCluster : ICluster\n{\n    public void UnlockForEepromReadWrite()\n    {\n        var (isUnlocked, softwareVersion) = Unlock();\n        if (!isUnlocked)\n        {\n            Log.WriteLine(\"Unknown cluster software version. EEPROM access will likely fail.\");\n        }\n\n        if (!RequiresSeedKey())\n        {\n            Log.WriteLine(\n                \"Cluster is unlocked for ROM/EEPROM access. Skipping Seed/Key login.\");\n            return;\n        }\n\n        SeedKeyAuthenticate(softwareVersion);\n        if (RequiresSeedKey())\n        {\n            Log.WriteLine(\"Failed to unlock cluster.\");\n        }\n        else\n        {\n            Log.WriteLine(\"Cluster is unlocked for ROM/EEPROM access.\");\n        }\n    }\n\n    public string DumpEeprom(\n        uint? optionalAddress, uint? optionalLength, string? optionalFileName)\n    {\n        var address = optionalAddress ?? 0;\n        var length = optionalLength ?? 0x800;\n        var filename = optionalFileName ?? $\"VDO_0x{address:X6}_eeprom.bin\";\n\n        DumpEeprom((ushort)address, (ushort)length, maxReadLength: 16, filename);\n\n        return filename;\n    }\n\n    /// <summary>\n    /// http://www.maltchev.com/kiti/VAG_guide.txt\n    /// </summary>\n    public Dictionary<int, Block> CustomReadSoftwareVersion()\n    {\n        var versionBlocks = new Dictionary<int, Block>();\n\n        Log.WriteLine(\"Sending Custom \\\"Read Software Version\\\" blocks\");\n\n        // The cluster can return 4 variations of software version, specified by the 2nd byte\n        // of the block:\n        // 0x00 - Cluster software version\n        // 0x01 - Unknown\n        // 0x02 - Unknown\n        // 0x03 - Unknown\n        for (byte variation = 0x00; variation < 0x04; variation++)\n        {\n            var blocks = SendCustom([0x84, variation]);\n            foreach (var block in blocks.Where(b => !b.IsAckNak))\n            {\n                if (variation is 0x00 or 0x03)\n                {\n                    Log.WriteLine($\"{variation:X2}: {DumpMixedContent(block)}\");\n                }\n                else\n                {\n                    Log.WriteLine($\"{variation:X2}: {DumpBinaryContent(block)}\");\n                }\n                versionBlocks[variation] = block;\n            }\n        }\n\n        return versionBlocks;\n    }\n\n    public void CustomReset()\n    {\n        Log.WriteLine(\"Sending Custom Reset block\");\n        SendCustom([0x82]);\n    }\n\n    public List<byte> CustomReadMemory(uint address, byte count)\n    {\n        Log.WriteLine($\"Sending Custom \\\"Read Memory\\\" block (Address: ${address:X6}, Count: ${count:X2})\");\n        var blocks = SendCustom(\n        [\n            0x86,\n            count,\n            (byte)(address & 0xFF),\n            (byte)((address >> 8) & 0xFF),\n            (byte)((address >> 16) & 0xFF),\n        ]);\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            // Permissions issue?\n            return [];\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    /// <summary>\n    /// Read the low 64KB of the cluster's NEC controller ROM.\n    /// For MFA clusters, that should cover the entire ROM.\n    /// For FIS clusters, the ROM is 128KB and more work is needed to retrieve the high 64KB.\n    /// </summary>\n    /// <param name=\"address\"></param>\n    /// <param name=\"count\"></param>\n    /// <returns></returns>\n    public List<byte> CustomReadNecRom(ushort address, byte count)\n    {\n        Log.WriteLine($\"Sending Custom \\\"Read NEC ROM\\\" block (Address: ${address:X4}, Count: ${count:X2})\");\n        var blocks = SendCustom(\n        [\n            0xA6,\n            count,\n            (byte)(address & 0xFF),\n            (byte)((address >> 8) & 0xFF),\n        ]);\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            throw new InvalidOperationException($\"Custom \\\"Read NEC ROM\\\" returned {blocks.Count} blocks instead of 1\");\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    public List<byte> MapEeprom()\n    {\n        // Unlock partial EEPROM read\n        Unlock();\n\n        var map = new List<byte>();\n        const byte blockSize = 1;\n        for (ushort addr = 0; addr < 2048; addr += blockSize)\n        {\n            var blockBytes = _kwp1281.ReadEeprom(addr, blockSize);\n            blockBytes = Enumerable.Repeat(\n                blockBytes == null ? (byte)0 : (byte)0xFF,\n                blockSize).ToList();\n            map.AddRange(blockBytes);\n        }\n\n        return map;\n    }\n\n    public void DumpMem(string dumpFileName, uint startAddress, uint length)\n    {\n        const byte blockSize = 15;\n\n        bool succeeded = true;\n        using (var fs = File.Create(dumpFileName, blockSize, FileOptions.WriteThrough))\n        {\n            for (var addr = startAddress; addr < startAddress + length; addr += blockSize)\n            {\n                var readLength = (byte)Math.Min(startAddress + length - addr, blockSize);\n                var blockBytes = CustomReadMemory(addr, readLength);\n                if (blockBytes.Count != readLength)\n                {\n                    succeeded = false;\n                    blockBytes.AddRange(\n                        Enumerable.Repeat((byte)0, readLength - blockBytes.Count));\n                    Log.WriteLine($\"{readLength - blockBytes.Count} missing\");\n                }\n                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                fs.Flush();\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    private List<Block> SendCustom(List<byte> blockCustomBytes)\n    {\n        if (blockCustomBytes[0] > 0x80 && !_additionalCustomCommandsUnlocked)\n        {\n            CustomUnlockAdditionalCommands();\n            _additionalCustomCommandsUnlocked = true;\n        }\n\n        blockCustomBytes.Insert(0, (byte)BlockTitle.Custom);\n        _kwp1281.SendBlock(blockCustomBytes);\n        return _kwp1281.ReceiveBlocks();\n    }\n\n    public (bool succeeded, string? softwareVersion) Unlock()\n    {\n        var versionBlocks = CustomReadSoftwareVersion();\n        if (versionBlocks.Count == 0)\n        {\n            Log.WriteLine(\"Cluster did not return software version.\");\n            return (succeeded: false, softwareVersion: null);\n        }\n\n        // Now we need to send an unlock code that is unique to each ROM version\n        Log.WriteLine(\"Sending Custom \\\"Unlock partial EEPROM read\\\" block\");\n        var softwareVersion = SoftwareVersionToString(versionBlocks[0].Body);\n        var unlockCodes = GetClusterUnlockCodes(softwareVersion);\n        var unlocked = false;\n        foreach (var unlockCode in unlockCodes)\n        {\n            var unlockCommand = new List<byte> { 0x9D };\n            unlockCommand.AddRange(unlockCode);\n            var unlockResponse = SendCustom(unlockCommand);\n            if (unlockResponse.Count != 1)\n            {\n                throw new InvalidOperationException(\n                    $\"Received multiple responses from unlock request.\");\n            }\n            if (unlockResponse[0].IsAck)\n            {\n                Log.WriteLine(\n                    $\"Unlock code for software version '{softwareVersion}' is{Utils.Dump(unlockCode)}\");\n                if (unlockCodes.Length > 1)\n                {\n                    Log.WriteLine(\"Please report this to the program author.\");\n                }\n                unlocked = true;\n                break;\n            }\n            else if (!unlockResponse[0].IsNak)\n            {\n                throw new InvalidOperationException(\n                    $\"Received non-ACK/NAK ${unlockResponse[0].Title:X2} from unlock request.\");\n            }\n        }\n        return (unlocked, softwareVersion);\n    }\n\n    private const int MaxAccessLevel = 7;\n\n    /// <summary>\n    /// Tries to perform seed/key authentication with cluster.\n    /// </summary>\n    /// <param name=\"softwareVersion\">Software version string like \"VQMJ07LM 09.00\"</param>\n    public void SeedKeyAuthenticate(string? softwareVersion)\n    {\n        // Perform Seed/Key authentication\n        Log.WriteLine(\"Sending Custom \\\"Seed request\\\" block\");\n        var response = SendCustom([0x96, 0x01]);\n\n        var responseBlocks = response.Where(b => !b.IsAckNak).ToList();\n        if (responseBlocks is [CustomBlock customBlock])\n        {\n            Log.WriteLine($\"Block: {Utils.Dump(customBlock.Body)}\");\n\n            var keyBytes = VdoKeyFinder.FindKey(\n                customBlock.Body.ToArray(), MaxAccessLevel);\n\n            Log.WriteLine(\"Sending Custom \\\"Key response\\\" block\");\n\n            var keyResponse = new List<byte> { 0x96, 0x02 };\n            keyResponse.AddRange(keyBytes);\n\n            _ = SendCustom(keyResponse);\n        }\n    }\n\n    public bool RequiresSeedKey()\n    {\n        var accessLevel = GetAccessLevel();\n        return accessLevel != MaxAccessLevel;\n    }\n\n    private int? GetAccessLevel()\n    {\n        Log.WriteLine(\"Sending Custom \\\"Get Access Level\\\" block\");\n        var response = SendCustom([0x96, 0x04]);\n        var responseBlocks = response.Where(b => !b.IsAckNak).ToList();\n        if (responseBlocks is [CustomBlock])\n        {\n            int accessLevel = responseBlocks[0].Body.First();\n            Log.WriteLine($\"Access level is {accessLevel}.\");\n\n            return accessLevel;\n        }\n        else\n        {\n            Log.WriteLine(\"Access level is unknown.\");\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// Given a VDO cluster EEPROM dump, attempt to determine the SKC and return it if found.\n    /// </summary>\n    /// <param name=\"bytes\">A portion of a VDO cluster EEPROM dump.</param>\n    /// <param name=\"startAddress\">The start address of bytes within the EEPROM.</param>\n    /// <returns>The SKC or null if the SKC could not be determined.</returns>\n    public static ushort? GetSkc(byte[] bytes, int startAddress)\n    {\n        string text = Encoding.ASCII.GetString(bytes);\n\n        // There are several EEPROM formats. We can determine the format by locating the\n        // 14-character immobilizer ID and noting its offset in the dump.\n\n        var immoMatch = Regex.Match(\n            text,\n            @\"[A-Z]{2}Z\\dZ0[A-Z]\\d{7}\");\n        if (!immoMatch.Success)\n        {\n            Log.WriteLine(\"GetSkc: Unable to find Immobilizer ID in cluster dump.\");\n            return null;\n        }\n\n        ushort skc;\n        var index = immoMatch.Index + startAddress;\n\n        switch (index)\n        {\n            case 0x090:\n            case 0x0AC:\n                // Immo2\n                skc = Utils.GetBcd(bytes, 0x0BA - startAddress);\n                return skc;\n            case 0x0A2:\n                // VWK501\n                skc = Utils.GetShort(bytes, 0x0CC - startAddress);\n                return skc;\n            case 0x0E0:\n                // VWK503\n                skc = Utils.GetShort(bytes, 0x10A - startAddress);\n                return skc;\n            default:\n                Log.WriteLine(\n                    $\"GetSkc: Unknown EEPROM (Immobilizer offset: 0x{immoMatch.Index:X3})\");\n                return null;\n        }\n    }\n\n    /// <summary>\n    /// http://www.maltchev.com/kiti/VAG_guide.txt\n    /// This unlocks additional custom commands $81-$AF\n    /// </summary>\n    private void CustomUnlockAdditionalCommands()\n    {\n        Log.WriteLine(\"Sending Custom \\\"Unlock Additional Commands\\\" block\");\n        SendCustom([0x80, 0x01, 0x02, 0x03, 0x04]);\n    }\n\n    /// <summary>\n    /// Different cluster models have different unlock codes. Return the appropriate one based\n    /// on the cluster's software version.\n    /// </summary>\n    internal static byte[][] GetClusterUnlockCodes(string softwareVersion)\n    {\n        switch(softwareVersion)\n        {\n            case \"VT5P07MH 09.00\": // 7H5920872L VDO V03\n                return [[0x00, 0x07, 0x43, 0x35]];\n\n            case \"VAT500LL 01.00\":\n            case \"VAT500LL 01.20\": // 1J0920905L V01\n            case \"VAT500MH 01.10\": // 1J0920925D V06\n            case \"VAT500MH 01.20\": // 1J5920925C V09\n                return [[0x01, 0x04, 0x3D, 0x35]];\n\n            case \"$01 $00 $14 $01\": // 1J0919860B V15\n                return [[0x01, 0x08, 0x05, 0x02]];\n\n            case \"V798MLA 01.00\": // 7D0920800F V01, 1J0919951C V55\n                return [[0x02, 0x03, 0x05, 0x09]];\n\n            case \"$00 $00 $13 $01\": // 8D0919880M D02\n                return [[0x09, 0x06, 0x05, 0x02]];\n\n            case \"VSQX01LM 01.00\": // 6Q0920800 V11\n                return [[0x31, 0x39, 0x34, 0x46]];\n\n            case \"VCLM09MH $00 $09\": // 3BD920848E V03\n                return [[0x32, 0x31, 0x36, 0x31]];\n\n            case \"VCB07LL  09.00\": // 1JD920826E V01\n                return [[0x33, 0x34, 0x46, 0x4A]];\n\n            case \"VKQ501HH 09.00\":\n            case \"VQMJ07HH 08.40\": // 6Y0920843L V04\n            case \"VQMJ07LM 08.40\": // 6Q0920923Q V02\n            case \"VQMJ07LM 09.00\": // 6Q0920804Q V06\n                return [[0x34, 0x3F, 0x43, 0x39]];\n\n            case \"VQMJ06LM 09.00\": // 6Q0920903 V02\n                return [[0x35, 0x3D, 0x47, 0x3E]];\n\n            case \"SS5501LM 00.80\":\n            case \"SS5501ML 00.80\":\n                return [[0x36, 0x3B, 0x36, 0x3D]];\n\n            case \"VWK501LL 00.88\": // 1J0920906L V58\n            case \"VWK501MH 00.88\":\n            case \"VWK501LL 01.00\":\n            case \"VWK501MH 01.00\":\n                return [[0x36, 0x3D, 0x3E, 0x47]];\n\n            case \"VT5X02LL 09.40\":\n                return [[0x36, 0x3F, 0x45, 0x42]];\n\n            case \"VQMJ09HH 05.10\": // 6QE920827C V06\n                return [[0x37, 0x42, 0x47, 0x43]];\n\n            case \"VWK502MH 09.00\":\n                return [[0x38, 0x37, 0x3E, 0x31]];\n\n            case \"VT5X02LL 09.00\":\n                return [[0x38, 0x39, 0x3A, 0x47]];\n\n            case \"S599CAA  01.00\": // 1M0920800C V15\n            case \"V599HLA  00.91\": // 7D0920841A V18\n            case \"V599LLA  00.91\": // 7D0920801B V18\n            case \"V599LLA  01.00\": // 1J0920800L V59\n            case \"V599LLA  03.00\": // 1J0920900J V60\n            case \"V599MLA  01.00\": // 7D0920821D V22\n            case \"V599MLA  03.00\": // 3B0920920B V26\n                return [[0x38, 0x3F, 0x40, 0x35]];\n\n            case \"MPV300LL 04.00\":\n            case \"MPV501MH 01.00\": // 7M3920820H V57\n                return [[0x38, 0x47, 0x34, 0x3A]];\n\n            case \"VWK501MH 00.92\": // 3B0920827C V06\n            case \"VWK501MH 01.10\":\n                return [[0x39, 0x34, 0x34, 0x40]];\n\n            case \"VBK700LL 00.96\":\n            case \"VBK700LL 01.00\":\n            case \"VBKX00MH 01.00\":\n                return [[0x3A, 0x39, 0x31, 0x43]];\n\n            case \"MPV300LL 02.00\":\n                return [[0x3B, 0x47, 0x03, 0x02]];\n\n            case \"SS5501LM 01.00\": // 1M0920802D V05\n            case \"SS5501ML 01.00\":\n                return [[0x3C, 0x34, 0x47, 0x35]];\n\n            case \"VSQX01LM 01.20\":\n                return [[0x3D, 0x36, 0x40, 0x36]];\n\n            case \"S599CAA  00.80\":\n                return [[0x3D, 0x39, 0x3B, 0x35]];\n\n            case \"KB5M07HH 09.00\": // 3U0920842B V06\n            case \"VWK503LL 09.00\":\n            case \"VWK503MH 09.00\": // 1J0920927 V02\n                return [[0x3E, 0x35, 0x3D, 0x3A]];\n\n            case \"VMMJ08MH 09.00\": // 1J5920826L V75\n                return [[0x3E, 0x47, 0x3D, 0x48]];\n\n            case \"MPV300LL 00.90\":\n            case \"MPV500LL 00.90\":\n                return [[0x3F, 0x38, 0x43, 0x38]];\n\n            case \"SS5500LM 01.00\":\n                return [[0x40, 0x39, 0x39, 0x38]];\n\n            case \"VSQX01LM 01.10\": // 6Q0920900 V18\n                return [[0x43, 0x43, 0x3D, 0x37]];\n\n            case \"MPV300LL 03.00\":\n                return [[0x43, 0x43, 0x43, 0x39]];\n\n            case \"KPQMLA` $01\": // 6Y1920860G V12\n                return [[0x47, 0x3B, 0x31, 0x3F]];\n\n            case \"K5MJ07HH 09.00\": // 5J0920840B V92\n            case \"K5MJ07LM 08.10\": // 5J0920810C V2721446\n            case \"K5MJ07LM 09.00\": // 5J0920900B V2823466\n                return [[0x47, 0x3F, 0x39, 0x44]];\n\n            default:\n                return ClusterUnlockCodes;\n        }\n    }\n\n    private static string SoftwareVersionToString(List<byte> versionBytes)\n    {\n        if (versionBytes.Count < 9 || versionBytes.Count > 10)\n        {\n            return Utils.DumpMixedContent(versionBytes);\n        }\n\n        var asciiPart = Encoding.ASCII.GetString(versionBytes.ToArray()[0..^2]);\n        return $\"{asciiPart} {versionBytes[^1]:X2}.{versionBytes[^2]:X2}\";\n    }\n\n    internal static readonly byte[][] ClusterUnlockCodes =\n    [\n        [0x00, 0x00, 0x00, 0x00],\n        [0x00, 0x00, 0x03, 0x02],\n        [0x00, 0x01, 0x03, 0x02],\n        [0x00, 0x02, 0x03, 0x02],\n        [0x00, 0x02, 0x09, 0x07],\n        [0x00, 0x03, 0x03, 0x02],\n        [0x00, 0x03, 0x04, 0x02],\n        [0x00, 0x04, 0x03, 0x02],\n        [0x00, 0x04, 0x06, 0x07],\n        [0x00, 0x05, 0x03, 0x02],\n        [0x00, 0x06, 0x03, 0x02],\n        [0x00, 0x07, 0x02, 0x04],\n        [0x00, 0x07, 0x03, 0x08],\n        [0x00, 0x07, 0x43, 0x35],\n        [0x00, 0x08, 0x02, 0x04],\n        [0x01, 0x00, 0x03, 0x02],\n        [0x01, 0x00, 0x09, 0x05],\n        [0x01, 0x01, 0x00, 0x04],\n        [0x01, 0x01, 0x00, 0x05],\n        [0x01, 0x01, 0x00, 0x06],\n        [0x01, 0x01, 0x00, 0x07],\n        [0x01, 0x01, 0x00, 0x08],\n        [0x01, 0x01, 0x00, 0x09],\n        [0x01, 0x01, 0x01, 0x00],\n        [0x01, 0x01, 0x01, 0x01],\n        [0x01, 0x01, 0x01, 0x02],\n        [0x01, 0x01, 0x01, 0x03],\n        [0x01, 0x01, 0x01, 0x04],\n        [0x01, 0x01, 0x01, 0x05],\n        [0x01, 0x01, 0x01, 0x06],\n        [0x01, 0x01, 0x03, 0x02],\n        [0x01, 0x01, 0x03, 0x07],\n        [0x01, 0x01, 0x05, 0x08],\n        [0x01, 0x01, 0x07, 0x09],\n        [0x01, 0x02, 0x03, 0x02],\n        [0x01, 0x03, 0x03, 0x02],\n        [0x01, 0x04, 0x02, 0x02],\n        [0x01, 0x04, 0x03, 0x02],\n        [0x01, 0x04, 0x3D, 0x35],\n        [0x01, 0x05, 0x03, 0x02],\n        [0x01, 0x05, 0x06, 0x08],\n        [0x01, 0x05, 0x3D, 0x35],\n        [0x01, 0x06, 0x00, 0x02],\n        [0x01, 0x06, 0x02, 0x00],\n        [0x01, 0x06, 0x03, 0x02],\n        [0x01, 0x06, 0x04, 0x02],\n        [0x01, 0x07, 0x00, 0x03],\n        [0x01, 0x08, 0x02, 0x05],\n        [0x01, 0x08, 0x03, 0x00],\n        [0x01, 0x08, 0x05, 0x02],\n        [0x02, 0x00, 0x03, 0x02],\n        [0x02, 0x00, 0x06, 0x01],\n        [0x02, 0x01, 0x03, 0x02],\n        [0x02, 0x02, 0x03, 0x02],\n        [0x02, 0x02, 0x04, 0x01],\n        [0x02, 0x02, 0x09, 0x02],\n        [0x02, 0x03, 0x03, 0x02],\n        [0x02, 0x03, 0x05, 0x09],\n        [0x02, 0x04, 0x00, 0x02],\n        [0x02, 0x04, 0x03, 0x02],\n        [0x02, 0x05, 0x00, 0x02],\n        [0x02, 0x05, 0x03, 0x02],\n        [0x02, 0x05, 0x06, 0x09],\n        [0x02, 0x05, 0x08, 0x01],\n        [0x02, 0x06, 0x03, 0x02],\n        [0x02, 0x06, 0x06, 0x09],\n        [0x02, 0x09, 0x02, 0x06],\n        [0x02, 0x09, 0x04, 0x02],\n        [0x02, 0x09, 0x04, 0x03],\n        [0x02, 0x32, 0x3B, 0x37],\n        [0x03, 0x00, 0x03, 0x02],\n        [0x03, 0x00, 0x03, 0x07],\n        [0x03, 0x00, 0x07, 0x01],\n        [0x03, 0x01, 0x03, 0x02],\n        [0x03, 0x02, 0x03, 0x02],\n        [0x03, 0x02, 0x05, 0x02],\n        [0x03, 0x03, 0x03, 0x02],\n        [0x03, 0x03, 0x08, 0x04],\n        [0x03, 0x03, 0x09, 0x03],\n        [0x03, 0x04, 0x03, 0x02],\n        [0x03, 0x05, 0x03, 0x02],\n        [0x03, 0x06, 0x03, 0x02],\n        [0x03, 0x08, 0x02, 0x05],\n        [0x04, 0x00, 0x03, 0x02],\n        [0x04, 0x01, 0x03, 0x02],\n        [0x04, 0x01, 0x03, 0x08],\n        [0x04, 0x02, 0x03, 0x02],\n        [0x04, 0x02, 0x06, 0x06],\n        [0x04, 0x03, 0x03, 0x02],\n        [0x04, 0x04, 0x03, 0x02],\n        [0x04, 0x04, 0x09, 0x04],\n        [0x04, 0x05, 0x03, 0x02],\n        [0x04, 0x05, 0x05, 0x02],\n        [0x04, 0x06, 0x03, 0x02],\n        [0x04, 0x07, 0x00, 0x07],\n        [0x05, 0x00, 0x03, 0x02],\n        [0x05, 0x01, 0x03, 0x02],\n        [0x05, 0x01, 0x04, 0x08],\n        [0x05, 0x02, 0x03, 0x02],\n        [0x05, 0x02, 0x03, 0x09],\n        [0x05, 0x02, 0x09, 0x02],\n        [0x05, 0x03, 0x03, 0x02],\n        [0x05, 0x04, 0x03, 0x02],\n        [0x05, 0x05, 0x03, 0x02],\n        [0x05, 0x05, 0x08, 0x09],\n        [0x05, 0x05, 0x09, 0x05],\n        [0x05, 0x06, 0x03, 0x02],\n        [0x05, 0x08, 0x05, 0x02],\n        [0x06, 0x00, 0x02, 0x02],\n        [0x06, 0x00, 0x03, 0x00],\n        [0x06, 0x00, 0x03, 0x02],\n        [0x06, 0x01, 0x03, 0x02],\n        [0x06, 0x02, 0x03, 0x02],\n        [0x06, 0x03, 0x03, 0x02],\n        [0x06, 0x04, 0x03, 0x02],\n        [0x06, 0x04, 0x07, 0x01],\n        [0x06, 0x05, 0x03, 0x02],\n        [0x06, 0x06, 0x03, 0x02],\n        [0x06, 0x06, 0x09, 0x06],\n        [0x06, 0x09, 0x01, 0x02],\n        [0x06, 0x09, 0x03, 0x09],\n        [0x06, 0x09, 0x05, 0x03],\n        [0x07, 0x00, 0x03, 0x02],\n        [0x07, 0x00, 0x06, 0x04],\n        [0x07, 0x01, 0x03, 0x02],\n        [0x07, 0x02, 0x03, 0x02],\n        [0x07, 0x03, 0x03, 0x02],\n        [0x07, 0x03, 0x05, 0x03],\n        [0x07, 0x04, 0x03, 0x02],\n        [0x07, 0x05, 0x03, 0x02],\n        [0x07, 0x06, 0x03, 0x02],\n        [0x07, 0x07, 0x09, 0x04],\n        [0x07, 0x07, 0x09, 0x07],\n        [0x08, 0x00, 0x03, 0x02],\n        [0x08, 0x01, 0x03, 0x02],\n        [0x08, 0x01, 0x06, 0x05],\n        [0x08, 0x02, 0x01, 0x04],\n        [0x08, 0x02, 0x03, 0x02],\n        [0x08, 0x02, 0x03, 0x05],\n        [0x08, 0x03, 0x03, 0x02],\n        [0x08, 0x04, 0x02, 0x02],\n        [0x08, 0x04, 0x03, 0x02],\n        [0x08, 0x05, 0x03, 0x02],\n        [0x08, 0x06, 0x03, 0x02],\n        [0x08, 0x06, 0x07, 0x06],\n        [0x08, 0x08, 0x09, 0x08],\n        [0x09, 0x00, 0x03, 0x02],\n        [0x09, 0x01, 0x01, 0x07],\n        [0x09, 0x01, 0x03, 0x02],\n        [0x09, 0x02, 0x03, 0x02],\n        [0x09, 0x02, 0x06, 0x06],\n        [0x09, 0x03, 0x03, 0x02],\n        [0x09, 0x03, 0x09, 0x06],\n        [0x09, 0x04, 0x03, 0x02],\n        [0x09, 0x05, 0x02, 0x03],\n        [0x09, 0x05, 0x03, 0x02],\n        [0x09, 0x05, 0x05, 0x08],\n        [0x09, 0x06, 0x03, 0x02],\n        [0x09, 0x06, 0x04, 0x09],\n        [0x09, 0x06, 0x05, 0x02],\n        [0x09, 0x09, 0x03, 0x02],\n        [0x09, 0x09, 0x09, 0x09],\n        [0x31, 0x39, 0x34, 0x46],\n        [0x31, 0x44, 0x35, 0x43],\n        [0x32, 0x31, 0x36, 0x31],\n        [0x32, 0x37, 0x3E, 0x31],\n        [0x33, 0x34, 0x46, 0x4A],\n        [0x34, 0x3F, 0x43, 0x39],\n        [0x35, 0x3B, 0x39, 0x3D],\n        [0x35, 0x3C, 0x31, 0x3C],\n        [0x35, 0x3D, 0x04, 0x01],\n        [0x35, 0x3D, 0x47, 0x3E],\n        [0x35, 0x40, 0x3F, 0x38],\n        [0x35, 0x43, 0x31, 0x38],\n        [0x35, 0x47, 0x34, 0x3C],\n        [0x36, 0x3B, 0x36, 0x3D],\n        [0x36, 0x3D, 0x3E, 0x47],\n        [0x36, 0x3F, 0x45, 0x42],\n        [0x36, 0x40, 0x36, 0x3D],\n        [0x37, 0x39, 0x3C, 0x47],\n        [0x37, 0x3B, 0x32, 0x02],\n        [0x37, 0x3D, 0x43, 0x43],\n        [0x37, 0x42, 0x47, 0x43],\n        [0x38, 0x34, 0x34, 0x37],\n        [0x38, 0x37, 0x3E, 0x31],\n        [0x38, 0x39, 0x39, 0x40],\n        [0x38, 0x39, 0x3A, 0x47],\n        [0x38, 0x3F, 0x40, 0x35],\n        [0x38, 0x43, 0x38, 0x3F],\n        [0x38, 0x47, 0x34, 0x3A],\n        [0x39, 0x34, 0x34, 0x40],\n        [0x39, 0x43, 0x43, 0x43],\n        [0x3A, 0x31, 0x31, 0x36],\n        [0x3A, 0x34, 0x47, 0x38],\n        [0x3A, 0x39, 0x31, 0x43],\n        [0x3A, 0x39, 0x41, 0x43],\n        [0x3A, 0x3B, 0x35, 0x3C],\n        [0x3A, 0x3B, 0x35, 0x4C],\n        [0x3A, 0x3D, 0x35, 0x3E],\n        [0x3B, 0x33, 0x3E, 0x37],\n        [0x3B, 0x3A, 0x37, 0x3E],\n        [0x3B, 0x46, 0x23, 0x10],\n        [0x3B, 0x46, 0x23, 0x1B],\n        [0x3B, 0x46, 0x23, 0x1D],\n        [0x3B, 0x47, 0x03, 0x02],\n        [0x3C, 0x31, 0x3C, 0x35],\n        [0x3C, 0x34, 0x47, 0x35],\n        [0x3D, 0x36, 0x40, 0x36],\n        [0x3D, 0x39, 0x3B, 0x35],\n        [0x3E, 0x35, 0x3D, 0x3A],\n        [0x3E, 0x35, 0x43, 0x30],\n        [0x3E, 0x35, 0x43, 0x39],\n        [0x3E, 0x35, 0x43, 0x40],\n        [0x3E, 0x35, 0x43, 0x41],\n        [0x3E, 0x35, 0x43, 0x42],\n        [0x3E, 0x35, 0x43, 0x43],\n        [0x3E, 0x35, 0x43, 0x44],\n        [0x3E, 0x39, 0x31, 0x43],\n        [0x3E, 0x39, 0x35, 0x40],\n        [0x3E, 0x39, 0x43, 0x34],\n        [0x3E, 0x3F, 0x40, 0x35],\n        [0x3E, 0x47, 0x3D, 0x48],\n        [0x3F, 0x31, 0x3B, 0x47],\n        [0x3F, 0x38, 0x43, 0x38],\n        [0x3F, 0x43, 0x35, 0x3E],\n        [0x40, 0x30, 0x3E, 0x39],\n        [0x40, 0x34, 0x34, 0x39],\n        [0x40, 0x39, 0x39, 0x38],\n        [0x40, 0x43, 0x35, 0x3E],\n        [0x41, 0x43, 0x35, 0x3E],\n        [0x42, 0x43, 0x35, 0x3E],\n        [0x42, 0x45, 0x3F, 0x36],\n        [0x43, 0x31, 0x39, 0x3A],\n        [0x43, 0x43, 0x35, 0x3E],\n        [0x43, 0x43, 0x3D, 0x37],\n        [0x43, 0x43, 0x43, 0x39],\n        [0x43, 0x45, 0x31, 0x3D],\n        [0x44, 0x43, 0x35, 0x3E],\n        [0x45, 0x39, 0x34, 0x43],\n        [0x47, 0x3A, 0x39, 0x38],\n        [0x47, 0x3B, 0x31, 0x3F],\n        [0x47, 0x3C, 0x39, 0x37],\n        [0x47, 0x3E, 0x3D, 0x36],\n        [0x47, 0x3F, 0x39, 0x44],\n    ];\n\n    private static string DumpMixedContent(Block block)\n    {\n        if (block.IsNak)\n        {\n            return \"NAK\";\n        }\n\n        return Utils.DumpMixedContent(block.Body);\n    }\n\n    private static string DumpBinaryContent(Block block)\n    {\n        if (block.IsNak)\n        {\n            return \"NAK\";\n        }\n\n        return Utils.DumpBytes(block.Body);\n    }\n\n    private void DumpEeprom(\n        ushort startAddr, ushort length, byte maxReadLength, string fileName)\n    {\n        bool succeeded = true;\n\n        using (var fs = File.Create(fileName, maxReadLength, FileOptions.WriteThrough))\n        {\n            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)\n            {\n                byte readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);\n                List<byte>? blockBytes = _kwp1281.ReadEeprom((ushort)addr, readLength);\n                if (blockBytes == null)\n                {\n                    blockBytes = Enumerable.Repeat((byte)0, readLength).ToList();\n                    succeeded = false;\n                }\n                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                fs.Flush();\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    public void WriteRam(ushort address, byte value)\n    {\n        Log.WriteLine(\"Sending Custom \\\"Write RAM\\\" block\");\n\n        SendCustom(\n            [\n                0x87,\n                1, // Count\n                (byte)(address & 0xFF),\n                (byte)((address >> 8) & 0xFF),\n                value\n            ]);\n    }\n\n    private readonly IKW1281Dialog _kwp1281;\n    private bool _additionalCustomCommandsUnlocked;\n\n    public VdoCluster(IKW1281Dialog kwp1281)\n    {\n        _kwp1281 = kwp1281;\n        _additionalCustomCommandsUnlocked = false;\n    }\n}\n"
  },
  {
    "path": "Cluster/VdoKeyFinder.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace BitFab.KW1281Test.Cluster\n{\n    public static class VdoKeyFinder\n    {\n        /// <summary>\n        /// Takes a 10-byte seed block, desired access level and optional cluster software version and generates an\n        /// 8-byte key block.\n        /// </summary>\n        public static byte[] FindKey(\n            byte[] seed, int accessLevel)\n        {\n            if (seed.Length != 10)\n            {\n                throw new InvalidOperationException(\n                    $\"Unexpected seed length: {seed.Length} (Expected 10)\");\n            }\n\n            byte[] secret;\n            switch (seed[8])\n            {\n                case 0x01 when seed[9] == 0x00:\n                    secret = Secrets0100[accessLevel];\n                    break;\n                case 0x03 when seed[9] == 0x00:\n                    secret = Secrets0300[accessLevel];\n                    break;\n                case 0x09 when seed[9] == 0x00:\n                    secret = Secrets0900[accessLevel];\n                    break;\n                case 0x0B when seed[9] == 0x00:\n                    secret = Secrets0B00[accessLevel];\n                    break;\n                case 0x0D when seed[9] == 0x00:\n                    secret = Secrets0D00[accessLevel];\n                    break;\n                default:\n                    Log.WriteLine(\n                        $\"Unexpected seed suffix: ${seed[8]:X2} ${seed[9]:X2}\");\n                    secret = Secrets0100[accessLevel]; // Try something\n                    break;\n            }\n\n            Log.WriteLine($\"Access level {accessLevel} secret: {Utils.DumpBytes(secret)}\");\n\n            var key = CalculateKey(\n                [seed[1], seed[3], seed[5], seed[7]],\n                secret);\n\n            return [(byte)accessLevel, key[0], key[1], 0x00, key[2], 0x00, key[3]];\n        }\n\n        /// <summary>\n        /// Table of secrets, one for each access level.\n        /// </summary>\n        private static readonly byte[][] Secrets0100 =\n        [\n            [0xe5, 0x7c, 0x20, 0xb3],   // AccessLevel 0\n            [0x67, 0xb8, 0xf0, 0xe2],\n            [0x59, 0xd0, 0x4f, 0xcb],\n            [0x46, 0x83, 0xb6, 0x27],\n            [0xc9, 0xde, 0xe3, 0xca],\n            [0x7f, 0x50, 0x44, 0xbc],\n            [0x4b, 0xd0, 0x7f, 0xad],\n            [0x55, 0x16, 0xa8, 0x94]    // AccessLevel 7\n        ];\n\n        /// <summary>\n        /// Table of secrets, one for each access level.\n        /// </summary>\n        private static readonly byte[][] Secrets0300 =\n        [\n            [0x4c, 0x29, 0x92, 0x1b],   // AccessLevel 0\n            [0x42, 0x0a, 0x0b, 0x66],\n            [0x1c, 0x4c, 0x91, 0x4d],\n            [0xe2, 0xfd, 0xa2, 0x28],\n            [0x48, 0x34, 0x58, 0x71],\n            [0xb1, 0xf5, 0xd0, 0xb8],\n            [0xac, 0xfc, 0x5e, 0x6c],\n            [0x98, 0xe1, 0x56, 0x5f]    // AccessLevel 7\n        ];\n\n        /// <summary>\n        /// Table of secrets, one for each access level.\n        /// </summary>\n        private static readonly byte[][] Secrets0900 =\n        [\n            [0xa7, 0xd2, 0xe9, 0x8d],  // AccessLevel 0\n            [0xe6, 0xfa, 0x9e, 0xba],\n            [0x63, 0x92, 0xe3, 0x08],\n            [0x55, 0x3e, 0x68, 0x24],\n            [0x03, 0x2a, 0x70, 0xdc],\n            [0xe7, 0xb4, 0x71, 0x86],\n            [0x4f, 0x58, 0xcd, 0x81],\n            [0xfd, 0x8e, 0x31, 0x96]    // AccessLevel 7\n        ];\n\n        /// <summary>\n        /// Table of secrets, one for each access level.\n        /// </summary>\n        private static readonly byte[][] Secrets0D00 =\n        [\n            [0xc9, 0x18, 0xe6, 0x6e],  // AccessLevel 0\n            [0x69, 0xc3, 0x08, 0xcd],\n            [0x37, 0x15, 0xd3, 0x23],\n            [0xe1, 0xe1, 0xa9, 0x3b],\n            [0x19, 0x74, 0x72, 0x18],\n            [0x08, 0x2b, 0x49, 0x1a],\n            [0x82, 0xd1, 0x7d, 0x50],\n            [0x0a, 0x5b, 0x41, 0x4f]    // AccessLevel 7\n        ];\n\n        private static readonly byte[][] Secrets0B00 =\n        [\n            [0x47, 0x36, 0x9a, 0xbb],   // AccessLevel 0\n            [0xad, 0x4e, 0x61, 0x44],\n            [0xd3, 0xd6, 0x42, 0x59],\n            [0x13, 0x6f, 0x43, 0x74],\n            [0xfc, 0xb8, 0x59, 0x2e],\n            [0x09, 0x58, 0x9d, 0x7f],\n            [0x24, 0x27, 0xc3, 0x9d],\n            [0x87, 0xed, 0x34, 0x63]    // AccessLevel 7\n        ];\n\n        /// <summary>\n        /// Takes a 4-byte seed and calculates a 4-byte key.\n        /// </summary>\n        private static byte[] CalculateKey(\n            IReadOnlyList<byte> seed,\n            IReadOnlyList<byte> secret)\n        {\n            var work = new byte[] { seed[0], seed[1], seed[2], seed[3], 0x00, 0x00 };\n            var secretBuf = secret.ToArray();\n\n            Scramble(work);\n\n            var y = work[0] & 0x07;\n            var temp = y + 1;\n\n            var a = LeftRotate(0x01, y);\n\n            do\n            {\n                var set = ((secretBuf[0] ^ secretBuf[1] ^ secretBuf[2] ^ secretBuf[3]) & 0x40) != 0;\n                secretBuf[3] = SetOrClearBits(secretBuf[3], a, set);\n\n                RightRotateFirst4Bytes(secretBuf, 0x01);\n                temp--;\n            }\n            while (temp != 0);\n\n            for (var x = 0; x < 2; x++)\n            {\n                work[4] = work[0];\n                work[0] ^= work[2];\n\n                work[5] = work[1];\n                work[1] ^= work[3];\n\n                work[3] = work[5];\n                work[2] = work[4];\n\n                LeftRotateFirstTwoBytes(work, work[2] & 0x07);\n\n                y = x << 1;\n\n                var carry = true;\n                (work[0], carry) = Utils.SubtractWithCarry(work[0], secretBuf[y], carry);\n                (work[1], _) = Utils.SubtractWithCarry(work[1], secretBuf[y + 1], carry);\n            }\n\n            Scramble(work);\n\n            return [work[0], work[1], work[2], work[3]];\n        }\n\n        private static void Scramble(byte[] work)\n        {\n            work[4] = work[0];\n            work[0] = work[1];\n            work[1] = work[3];\n            work[3] = work[2];\n            work[2] = work[4];\n        }\n\n        private static byte SetOrClearBits(\n            byte value, byte mask, bool set)\n        {\n            if (set)\n            {\n                return (byte)(value | mask);\n            }\n            else\n            {\n                return (byte)(value & (byte)(mask ^ 0xFF));\n            }\n        }\n\n        /// <summary>\n        /// Right-Rotate the first 4 bytes of a buffer count times.\n        /// </summary>\n        private static void RightRotateFirst4Bytes(\n            byte[] buf, int count)\n        {\n            while (count != 0)\n            {\n                var carry = (buf[0] & 0x01) != 0;\n                (buf[3], carry) = Utils.RightRotate(buf[3], carry);\n                (buf[2], carry) = Utils.RightRotate(buf[2], carry);\n                (buf[1], carry) = Utils.RightRotate(buf[1], carry);\n                (buf[0], _) = Utils.RightRotate(buf[0], carry);\n                count--;\n            }\n        }\n\n        private static void LeftRotateFirstTwoBytes(\n            byte[] work, int count)\n        {\n            while (count > 0)\n            {\n                var carry = (work[1] & 0x80) != 0;\n                (work[0], carry) = Utils.LeftRotate(work[0], carry);\n                (work[1], _) = Utils.LeftRotate(work[1], carry);\n                count--;\n            }\n        }\n\n        /// <summary>\n        /// Left-Rotate a value count-times.\n        /// </summary>\n        private static byte LeftRotate(\n            byte value, int count)\n        {\n            while (count != 0)\n            {\n                var carry = (value & 0x80) != 0;\n                (value, _) = Utils.LeftRotate(value, carry);\n                count--;\n            }\n\n            return value;\n        }\n    }\n}\n"
  },
  {
    "path": "ControllerAddress.cs",
    "content": "﻿namespace BitFab.KW1281Test\n{\n    /// <summary>\n    /// VW controller addresses\n    /// </summary>\n    enum ControllerAddress\n    {\n        Ecu = 0x01,\n        CentralElectric = 0x09,\n        Cluster = 0x17,\n        CanGateway = 0x19,\n        Immobilizer = 0x25,\n        CentralLocking = 0x35,\n        Navigation = 0x37,\n        CCM = 0x46,\n        Radio = 0x56,\n        RadioManufacturing = 0x7C,\n    }\n}\n"
  },
  {
    "path": "ControllerIdent.cs",
    "content": "﻿using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace BitFab.KW1281Test\n{\n    /// <summary>\n    /// The info returned by the controller to a ReadIdent block.\n    /// </summary>\n    internal class ControllerIdent\n    {\n        public ControllerIdent(IEnumerable<Block> blocks)\n        {\n            var sb = new StringBuilder();\n            foreach (var block in blocks)\n            {\n                if (block is AsciiDataBlock asciiBlock)\n                {\n                    sb.Append(asciiBlock);\n                }\n                else if (block is CodingWscBlock codingWscBlock)\n                {\n                    sb.AppendLine();\n                    sb.Append(codingWscBlock);\n                }\n                else\n                {\n                    Log.WriteLine($\"ReadIdent returned block of type {block.GetType()}\");\n                }\n            }\n            Text = sb.ToString();\n        }\n\n        public string Text { get; }\n\n        public override string ToString()\n        {\n            return Text;\n        }\n    }\n}"
  },
  {
    "path": "ControllerInfo.cs",
    "content": "﻿using BitFab.KW1281Test.Blocks;\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace BitFab.KW1281Test\n{\n    /// <summary>\n    /// The info returned when a controller wakes up.\n    /// </summary>\n    internal class ControllerInfo\n    {\n        public ControllerInfo(IEnumerable<Block> blocks)\n        {\n            var sb = new StringBuilder();\n            foreach (var block in blocks)\n            {\n                if (block is AsciiDataBlock asciiBlock)\n                {\n                    sb.Append(asciiBlock);\n                    if (asciiBlock.MoreDataAvailable)\n                    {\n                        MoreDataAvailable = true;\n                    }\n                }\n                else if (block is CodingWscBlock codingBlock)\n                {\n                    sb.Append($\"{Environment.NewLine}{codingBlock}\");\n                    SoftwareCoding = codingBlock.SoftwareCoding;\n                    WorkshopCode = codingBlock.WorkshopCode;\n                }\n                else\n                {\n                    Log.WriteLine($\"Controller wakeup returned block of type {block.GetType()}\");\n                }\n            }\n            Text = sb.ToString();\n        }\n\n        public string Text { get; }\n\n        public bool MoreDataAvailable { get; }\n\n        public int SoftwareCoding { get; }\n\n        public int WorkshopCode { get; }\n\n        public override string ToString()\n        {\n            return Text;\n        }\n    }\n}\n"
  },
  {
    "path": "EDC15/Edc15VM.cs",
    "content": "﻿using BitFab.KW1281Test.Kwp2000;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading;\n\nnamespace BitFab.KW1281Test.EDC15\n{\n    public class Edc15VM\n    {\n        public byte[] ReadWriteEeprom(\n            string filename,\n            List<KeyValuePair<ushort, byte>>? addressValuePairs = null)\n        {\n            addressValuePairs ??= [];\n\n            var kwp2000 = new KW2000Dialog(_kwpCommon, (byte)_controllerAddress);\n\n            _ = kwp2000.SendReceive(DiagnosticService.startDiagnosticSession, [0x89]);\n\n            _ = kwp2000.SendReceive(DiagnosticService.startDiagnosticSession, [0x85]);\n\n            const byte accMod = 0x41;\n            var resp = kwp2000.SendReceive(DiagnosticService.securityAccess, [accMod]);\n\n            // ECU normally doesn't require seed/key authentication the first time it wakes up in\n            // KWP2000 mode so sending an empty key is sufficient.\n            var buf = new List<byte> { accMod + 1 };\n\n            if (!resp.Body.SequenceEqual(new byte[] { accMod, 0x00, 0x00 }))\n            {\n                // Normally we'll only get here if we wake up the ECU and it's already in KWP2000 mode,\n                // which can happen if a previous download attempt did not complete. In that case we\n                // need to calculate and send back a real key.\n                var seedBuf = resp.Body.Skip(1).Take(4).ToArray();\n                var keyBuf = LVL41Auth(0x508DA647, 0x3800000, seedBuf);\n\n                buf.AddRange(keyBuf);\n            }\n            _ = kwp2000.SendReceive(DiagnosticService.securityAccess, buf.ToArray());\n\n            var loader = Edc15VM.GetLoader();\n            var len = loader.Length;\n\n            // Ask the ECU to accept our loader and store it in RAM\n            _ = kwp2000.SendReceive(DiagnosticService.requestDownload, [\n                0x40, 0xE0, 0x00, // Load address 0x40E000\n                0x00, // Not compressed, not encrypted\n                (byte)(len >> 16), (byte)(len >> 8), (byte)(len & 0xFF) // Length\n                ],\n            excludeAddresses: true);\n\n            // Break the loader into blocks and send each one\n            var maxBlockLen = resp.Body[0];\n            var s = new MemoryStream(loader);\n            while (true)\n            {\n                Thread.Sleep(5);\n\n                var blockBytes = new byte[maxBlockLen];\n                var readCount = s.Read(blockBytes, 0, maxBlockLen - 1);\n                if (readCount == 0)\n                {\n                    break;\n                }\n\n                _ = kwp2000.SendReceive(\n                    DiagnosticService.transferData, blockBytes.Take(readCount).ToArray(),\n                    excludeAddresses: true);\n            }\n\n            // Ask the ECU to execute our loader\n            kwp2000.SendMessage(\n                DiagnosticService.startRoutineByLocalIdentifier, [0x02],\n                excludeAddresses: true);\n            _ = kwp2000.ReceiveMessage();\n\n            // Custom loader command to send all 512 bytes of the EEPROM\n            kwp2000.SendMessage(\n                (DiagnosticService)0xA6, [],\n                excludeAddresses: true);\n            resp = kwp2000.ReceiveMessage();\n            if (!resp.IsPositiveResponse(DiagnosticService.transferData))\n            {\n                throw new InvalidOperationException($\"Dump EEPROM failed.\");\n            }\n\n            var eeprom = new byte[512];\n            for (var i = 0; i < 512; i++)\n            {\n                eeprom[i] = _kwpCommon.Interface.ReadByte();\n            }\n\n            File.WriteAllBytes(filename, eeprom);\n            Log.WriteLine($\"Saved EEPROM to {filename}\");\n\n            _ = kwp2000.ReceiveMessage();\n\n            // Now write any supplied values\n            foreach (var addressValuePair in addressValuePairs)\n            {\n                var service = (DiagnosticService)(\n                    addressValuePair.Key > 0xFF\n                        ? 0xA8  // Write 1 byte to EEPROM (Page 1)\n                        : 0xA7); // Write 1 byte to EEPROM (Page 0)\n\n                kwp2000.SendMessage(\n                    service, [],\n                    excludeAddresses: true);\n                resp = kwp2000.ReceiveMessage();\n                if (!resp.IsPositiveResponse(DiagnosticService.transferData))\n                {\n                    throw new InvalidOperationException($\"Write EEPROM failed.\");\n                }\n\n                var address = (byte)(addressValuePair.Key & 0xFF);\n                var value = addressValuePair.Value;\n\n                _kwpCommon.WriteByte(address);\n                _kwpCommon.WriteByte(value);\n                Log.WriteLine($\"Sent: {address:X2} {value:X2}\");\n\n                resp = kwp2000.ReceiveMessage();\n                if (!resp.IsPositiveResponse(DiagnosticService.transferData))\n                {\n                    throw new InvalidOperationException($\"Write EEPROM failed.\");\n                }\n            }\n\n            // Custom loader command to reboot the ECU to return it to normal operation.\n            kwp2000.SendMessage(\n                    (DiagnosticService)0xA2, [],\n                excludeAddresses: true);\n            _ = kwp2000.ReceiveMessage();\n\n            var b = _kwpCommon.Interface.ReadByte();\n            if (b == 0x55)\n            {\n                Log.WriteLine($\"Reboot successful!\");\n            }\n\n            return eeprom;\n        }\n\n        public static void DisplayEepromInfo(ReadOnlySpan<byte> eeprom)\n        {\n            var skc = Utils.GetShort(eeprom, 0x12E);\n            Log.WriteLine($\"SKC: {skc:D5}\");\n\n            double odometerKm =\n                eeprom[0x1BF] +\n                (eeprom[0x1C0] << 8) +\n                (eeprom[0x1C1] << 16) +\n                ((eeprom[0x1C2] & 0x3F) << 24);\n            odometerKm /= 100.0;\n            Log.WriteLine($\"Odometer: {odometerKm} km\");\n\n            var vin = Utils.DumpAscii(eeprom.Slice(0x140, 17).ToArray());\n            Log.WriteLine($\"VIN: {vin}\");\n\n            var immoNumber = Utils.DumpAscii(eeprom.Slice(0x131, 14).ToArray());\n            Log.WriteLine($\"Immo Number: {immoNumber}\");\n\n            var immoId = Utils.DumpBytes(eeprom.Slice(0x126, 7).ToArray());\n            Log.WriteLine($\"Immo Id: {immoId}\");\n\n            const ushort immo1Addr = 0x1B0;\n            var immo1 = eeprom[immo1Addr];\n            const ushort immo2Addr = 0x1DE;\n            var immo2 = eeprom[immo2Addr];\n            var immoStatus = immo1 == 0x60 && immo2 == 0x60 ? \"Off\" : \"On\";\n            Log.WriteLine($\"Immo is {immoStatus} (${immo1Addr:X3}=${immo1:X2}, ${immo2Addr:X3}=${immo2:X2})\");\n        }\n\n        /// <summary>\n        /// This algorithm borrowed from https://github.com/fjvva/ecu-tool\n        /// Thanks to Javier Vazquez Vidal https://github.com/fjvva\n        /// </summary>\n        private static byte[] LVL41Auth(long key, long key3, byte[] buf)\n        {\n            // long Key3 = 0x3800000;\n            long tempstring = buf[0];\n            tempstring <<= 8;\n            var keyread1 = tempstring + buf[1];\n            tempstring = buf[2];\n            tempstring <<= 8;\n            var keyread2 = tempstring + buf[3];\n            // Process the algorithm\n            var key2 = key;\n            key2 &= 0xFFFF;\n            key >>= 16;\n            var key1 = key;\n            for (byte counter = 0; counter < 5; counter++)\n            {\n                var keyTemp = keyread1;\n                keyTemp &= 0x8000;\n                keyread1 <<= 1;\n                var temp1 = keyTemp & 0x0FFFF;\n                if (temp1 == 0)\n                {\n                    var temp2 = keyread2 & 0xFFFF;\n                    var temp3 = keyTemp & 0xFFFF0000;\n                    keyTemp = temp2 + temp3;\n                    keyread1 &= 0xFFFE;\n                    temp2 = keyTemp & 0xFFFF;\n                    temp2 >>= 0x0F;\n                    keyTemp &= 0xFFFF0000;\n                    keyTemp += temp2;\n                    keyread1 |= keyTemp;\n                    keyread2 <<= 0x01;\n                }\n                else\n                {\n                    keyTemp = keyread2 + keyread2;\n                    keyread1 &= 0xFFFE;\n                    var temp2 = keyTemp & 0xFF;\n                    temp2 |= 1;\n                    var temp3 = key3 & 0xFFFFFF00;\n                    key3 = temp2 + temp3;\n                    key3 &= 0xFFFF00FF;\n                    key3 |= keyTemp;\n                    temp2 = keyread2 & 0xFFFF;\n                    temp3 = keyTemp & 0xFFFF0000;\n                    keyTemp = temp2 + temp3;\n                    temp2 = keyTemp & 0xFFFF;\n                    temp2 >>= 0x0F;\n                    keyTemp &= 0xFFFF0000;\n                    keyTemp += temp2;\n                    keyTemp |= keyread1;\n                    key3 ^= key1;\n                    keyTemp ^= key2;\n                    keyread2 = key3;\n                    keyread1 = keyTemp;\n                }\n            }\n            //Done with the key generation\n            keyread2 &= 0xFFFF; // Clean first and second word from garbage\n            keyread1 &= 0xFFFF;\n\n            var keybuf = new byte[4];\n            keybuf[1] = (byte)keyread1;\n            keyread1 >>= 8;\n            keybuf[0] = (byte)keyread1;\n            keybuf[3] = (byte)keyread2;\n            keyread2 >>= 8;\n            keybuf[2] = (byte)keyread2;\n\n            return keybuf;\n        }\n\n        /// <summary>\n        /// Loader that can read/write the serial EEPROM.\n        /// </summary>\n        private static byte[] GetLoader()\n        {\n            var assembly = Assembly.GetEntryAssembly()!;\n            var resourceStream = assembly.GetManifestResourceStream(\n                \"BitFab.KW1281Test.EDC15.Loader.bin\");\n            if (resourceStream == null)\n            {\n                throw new InvalidOperationException(\n                    $\"Unable to load BitFab.KW1281Test.EDC15.Loader.bin embedded resource.\");\n            }\n\n            var loaderLength = resourceStream.Length + 4; // Add 4 bytes for checksum correction\n            loaderLength = (loaderLength + 7) / 8 * 8; // Round up to a multiple of 8 bytes\n            var buf = new byte[loaderLength];\n\n            resourceStream.ReadExactly(buf, 0, (int)resourceStream.Length);\n\n            // In order for this loader to be executed by the ECU, the checksum of all the bytes\n            // must be EFCD8631.\n\n            // Patch the loader with the location of the end (actually 1 byte past the end)\n            ushort loaderEnd = (ushort)(0xE000 + loaderLength);\n            buf[0x0E] = (byte)(loaderEnd & 0xFF);\n            buf[0x0F] = (byte)(loaderEnd >> 8);\n\n            // Take the checksum of the loader up to but not including the checksum correction\n            ushort r6 = 0xEFCD;\n            ushort r1 = 0x8631;\n            Checksum(ref r6, ref r1, buf.Take(buf.Length - 4).ToArray());\n\n            // Calculate the checksum correction bytes and insert them at the end of the loader\n            var padding = CalcPadding(r6, r1);\n            Array.Copy(padding, 0, buf, buf.Length - 4, 4);\n\n            return buf;\n        }\n\n        /// <summary>\n        /// Calculate the checksum correction padding needed to result in a checksum of EFCD8631\n        /// </summary>\n        /// <param name=\"r6\"></param>\n        /// <param name=\"r1\"></param>\n        /// <returns></returns>\n        private static byte[] CalcPadding(ushort r6, ushort r1)\n        {\n            var paddingH = (ushort)(0xDF9B ^ r6);\n            var paddingL = (ushort)(r1 - 0xAB85);\n\n            return\n            [\n                (byte)(paddingL & 0xFF),\n                (byte)(paddingL >> 8),\n                (byte)(paddingH & 0xFF),\n                (byte)(paddingH >> 8)\n            ];\n        }\n\n        /// <summary>\n        /// EDC15 checksum algorithm (sub_1584).\n        /// Calculates a 32-bit checksum of an array of bytes based on an initial 32-bit seed.\n        /// Based on https://www.ecuconnections.com/forum/viewtopic.php?f=211&t=49704&sid=5cf324c44d2c74d372984f428ffea5ed\n        /// </summary>\n        /// <param name=\"r6\">Input: High word of seed, Output: High word of checksum</param>\n        /// <param name=\"r1\">Input: Low word of seed, Output: Low word of checksum</param>\n        /// <param name=\"buf\">Buffer to calculate checksum for</param>\n        static void Checksum(ref ushort r6, ref ushort r1, byte[] buf)\n        {\n            int r3 = 0; // Buffer index\n            int r0 = buf.Length;\n            while (true)\n            {\n                r1 ^= GetBuf(buf, r3); r3 += 2;\n                r1 = Rol(r1, r6, out ushort c);\n                r6 = (ushort)(r6 - GetBuf(buf, r3) - c); r3 += 2;\n                r6 ^= r1;\n                if (r3 >= r0)\n                {\n                    break;\n                }\n\n                r1 = (ushort)(r1 - GetBuf(buf, r3) - 1); r3 += 2;\n                r1 += 0xDAAD;\n                r6 ^= GetBuf(buf, r3); r3 += 2;\n                r6 = Ror(r6, r1);\n                if (r3 >= r0)\n                {\n                    break;\n                }\n            }\n        }\n\n        /// <summary>\n        /// Rotates a 16-bit value right by count bits.\n        /// </summary>\n        private static ushort Ror(ushort value, ushort count)\n        {\n            count &= 0xF;\n            value = (ushort)((value >> count) | (value << (16 - count)));\n            return value;\n        }\n\n        /// <summary>\n        /// Rotates a 16-bit value left by count bits. Carry will be equal to the last bit rotated\n        /// or 0 if the low 4 bits of count are 0;\n        /// </summary>\n        private static ushort Rol(ushort value, ushort count, out ushort carry)\n        {\n            count &= 0xF;\n            value = (ushort)((value << count) | (value >> (16 - count)));\n            carry = ((value & 1) == 0 || (count == 0)) ? (ushort)0 : (ushort)1;\n            return value;\n        }\n\n        private static ushort GetBuf(byte[] buf, int ix)\n        {\n            return (ushort)(buf[ix] + (buf[ix + 1] << 8));\n        }\n\n        private readonly IKwpCommon _kwpCommon;\n        private readonly int _controllerAddress;\n\n        public Edc15VM(IKwpCommon kwpCommon, int controllerAddress)\n        {\n            _kwpCommon = kwpCommon;\n            _controllerAddress = controllerAddress;\n        }\n    }\n}\n"
  },
  {
    "path": "EDC15/Loader.a66",
    "content": "; Custom loader. When loaded at address 40E000 and started via startRoutineByLocalIdentifier 0x02,\n; it will accept several custom KWP2000-like commands via the K-Line to allow dumping the EEPROM\n; and other special functions.\n\n; The following Linux command will convert the Intel Hex-86 format output to raw binary:\n; srec_cat Loader.H86 -Intel -Output Loader.bin -Binary\n\n$M167\n$NOLI\n$INCLUDE (REG167.INC)\n$LI\n\nAll\t\tSECTION CODE AT 0E000H\n\t\tDB\t\t0A5H, 0A5H, 14H, 0E0H, 00H, 00H, 3CH, 0E0H\n\t\tDB\t\t00H, 00H\n\t\tDW\t\tRoutineStart\n\t\tDB\t\t00H, 00H\n\t\tDW\t\tRoutineEnd\n\t\tDB\t\t00H, 00H, 02H, 47H, 13H, 00H, 00H, 01H\n\t\tDB\t\t00H, 02H, 00H, 03H, 00H, 04H, 00H, 05H\n\t\tDB\t\t00H, 06H, 00H, 07H, 00H, 08H, 00H, 09H\n\t\tDB\t\t00H, 0AH, 00H, 0BH, 00H, 0CH, 00H, 0DH\n\t\tDB\t\t00H, 0EH, 00H, 0FH, 80H, 0FH, 0A0H, 0FH\n\t\tDB\t\t0C0H, 0FH, 00H, 10H, 45H, 2FH, 7DH, 64H\n\t\tDB\t\t9BH, 0C3H\n\n; Entry\nRoutineStart\tPROC FAR\n\t\tBCLR\tIEN\t\t\t; Disable interrupts\n\t\tBSET    S0REN\t\t; Receiver enabled\n\t\tMOV\t\tR0,#0E7FEH\t; \"SP\"\n\t\t\n\t\t; Receive and dispatch message\nE04A:\tCALL    E128\t; Receive message\n\n\t\tCMP     R6,#1\t\t; Timeout?\n\t\tJMPR    CC_Z,E0C6\n\n\t\tCMP     R6,#2 ; Bad checksum?\n\t\tJMPR    CC_Z,E082 ; Send NAK\n\t\t\n\t\tMOV     R1,#0E600H ; Receive buffer\n\t\tMOVB    RL2,[R1+#0001H] ; Service => RL2\n\t\t\n\t\tCMPB    RL2,#0023H ; 23: Read flash\n\t\tJMPR    CC_Z,Cmd23\n\n\t\tCMPB    RL2,#0036H ; 36: Program flash\n\t\tJMPR    CC_Z,Cmd36\n\t\t\n\t\tCMPB    RL2,#00A2H ; A2: Reboot\n\t\tJMPR    CC_Z,CmdA2\n\t\t\n\t\tCMPB    RL2,#00A3H ; A3: Respond with 0x55\n\t\tJMPR    CC_Z,CmdA3\n\n\t\tCMPB    RL2,#00A4H ; A4: Set baud rate\n\t\tJMPR    CC_Z,CmdA4\n\t\n\t\tCMPB    RL2,#00A5H ; A5: Erase flash\n\t\tJMPR    CC_Z,CmdA5\n\t\t\n\t\tCMPB    RL2,#00A6H ; A6: Dump EEPROM\n\t\tJMPR    CC_Z,CmdA6\n\n\t\tCMPB    RL2,#00A7H ; A7: Write 1 byte to EEPROM (Page 0)\n\t\tJMPR    CC_Z,CmdA7\n\n\t\tCMPB    RL2,#00A8H ; A8: Write 1 byte to EEPROM (Page 1)\n\t\tJMPR    CC_Z,CmdA8\n\nE082:\tCALL   \tSendNAK\n\t\tJMPR    CC_UC,E04A ; Receive and dispatch message\n\n; Read flash\nCmd23:\tCALL    E23E\n\t\tJMPR    CC_UC,E04A ; Receive and dispatch message\n\n; Program flash\nCmd36:\tCALL    E1C4 ; Program flash\nCmd36x:\tCALL    SendAck ; Send ACK\n\t\tJMPR    CC_UC,E04A ; Receive and dispatch message\n\n; Reboot\nCmdA2:\tCALL\tSendACK\n\t\tCALL   \tDELAY256\n\t\tMOVB    RL7,#0055H\n\t\tCALL\tXmitRL7\n\t\tSRST    ; Reboot\n\n; Respond with 0x55\nCmdA3:\tMOVB    RL7,#0055H\nCmdA3x:\tCALL    XmitRL7\n\t\tJMPR    CC_UC,E04A ; Receive and dispatch message\n\n; Set baud rate (buf[2] => S0BG)\nCmdA4:\tCALL    SendACK\n\t\tCALL    Delay256\n\t\tMOV     R2,#00H\n\t\tMOVB    RL2,[R1+#0002H]\n\t\tMOV     S0BG,R2\n\t\tMOVB    RL7,#00AAH\n\t\tJMPR    CC_UC,CmdA3x ; XmitRL7 + jump to dispatcher\n\n; Erase flash\nCmdA5:\tCALL    SendACK\n\t\tCALL    E2E6 ; Erase flash\n\t\tJMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher\n\n; Dump EEPROM\nCmdA6:\tCALL\tSendACK\n\t\tCALL   \tE1C6 ; Dump EEPROM\n\t\tJMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher\n\n; Write 1 byte to EEPROM (Page 0)\nCmdA7:\tCALL\tSendACK\n\t\tCALL   \tE324 ; Write 1 byte to EEPROM (Page 0)\n\t\tJMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher\n\n; Write 1 byte to EEPROM (Page 1)\nCmdA8:\tCALL\tSendACK\n\t\tCALL    E356\n\t\tJMPR    CC_UC,Cmd36x ; SendACK + jump to dispatcher\n\n; Handle timeout?\nE0C6:\tMOVB    RL7,[R1]\n\t\tADDB    RL7,#1\n\t\tJMPR    CC_UC,CmdA3x ; XmitRL7 + jump to dispatcher\n\nRoutineStart\tENDP\n\n; Send NAK\nSendNAK\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     R1,#0E600H ; Transmit buf\n\t\tMOVB    RL2,#01H ; Length 1\n\t\tMOVB    [R1],RL2\n\t\tMOVB    RL2,#007FH ; Code 7F (NAK)\n\t\tJMPR\tCC_UC,SendACK2\nSendNAK\tENDP\n\n; Send ACK\nSendACK\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     R1,#0E600H ; Transmit buf\n\t\tMOVB    RL2,#01H\n\t\tMOVB    [R1],RL2\n\t\tMOVB    RL2,#0076H ; Code 76 (transferData positive response)\nSendACK2:\t\t\n\t\tMOVB    [R1+#0001H],RL2\n\t\tCALL\tE15E ; Send message\n\t\tJMPR    CC_UC,E156x ; Pop R2,R1 + RET\nSendACK\tENDP\n\n; Receive message into buf (E600) - Status returned in R6 (0: Success, 1: Timeout, 2: Checksum error)\n; 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.\nE128\tPROC NEAR\n\t\tMOV\t\t[-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     [-R0],R3 ; Push R3\n\t\tMOV     R1,#0E600H ; Receive buf\n\t\tCALL    E192 ; Receive a byte in R7\n\t\tMOV     R2,#00H ; 0 => R2\n\t\tMOVB    RL2,RL7 ; RL7 => RL2 (RL2 is message length)\n\t\tMOVB    RL3,RL7 ; RL7 => RL3 (RL3 is message checksum)\n\t\tMOVB    [R1],RL7 ; Put received byte in buf\n\t\tADD     R1,#1 ; Advance to next buf location\nE140:\tCALL    E1A2 ; Receive a byte in R7. R6: 0 if success, 1 if timeout\n\t\tCMP     R6,#0 ; Success?\n\t\tJMPR    CC_NZ,E156 ; Return if error\n\t\tMOVB    [R1],RL7 ; Put received byte in buf\n\t\tADDB    RL3,RL7 ; RL3 += RL7\n\t\tADD     R1,#1 ; Advance to next buf location\n\t\tCMPD1   R2,#00H ; (R2-- == 0)?\n\t\tJMPR    CC_NZ,E140 ; Loop if not\n\t\tSUBB    RL3,RL7 ; RL3 - RL7 => RL3\n\t\tCMPB    RL3,RL7 ; (RL3 == RL7)?\n\t\tJMPR    CC_Z,E156 ; Return if true\n\t\tMOV     R6,#02H ; Checksum error\nE156:\tMOV     R3,[R0+] ; Pop R3\nE156x:\tMOV     R2,[R0+] ; Pop R2\nE156y:\tMOV     R1,[R0+] ; Pop R1\n\t\tRET\nE128\tENDP\n\n; Send message in buf (E600)\nE15E\tPROC NEAR\n\t\tMOV\t\t[-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     [-R0],R3 ; Push R3\n\t\tMOV     R1,#0E600H ; Transmit buf\n\t\tMOV     R2,#00H\n\t\tMOVB    RL2,[R1] ; RL2 is length\n\t\tMOVB    RL3,#00H ; R3 is checksum\nE16E:\tMOVB    RL7,[R1+] ; message byte => RL7\n\t\tADDB    RL3,RL7 ; checksum += byte\n\t\tCALL    XmitRL7\n\t\tCMPD1   R2,#00H\n\t\tJMPR    CC_NZ,E16E ; Loop until all characters sent\n\t\tMOVB    RL7,RL3 ; checksum => RL7\n\t\tCALL    XmitRL7\n\t\tJMPR    CC_UC,E156 ; Pop R3,R2,R1 + RET\nE15E\tENDP\n\n\t\t; Transmit byte in R7\nXmitRL7\tPROC NEAR\n\t\tMOVB\tS0TBUF,RL7\t; Transmit RL7\n\t\tJMPR\tCC_UC,E192x\t; Receive echo in R7\nXmitRL7\tENDP\n\n\t\t; Receive a byte in R7\nE192\tPROC NEAR\nE192x:\n\t\tSRVWDT\t\t\t\t\t; Feed watchdog\n\t\tJNB\t\tS0RIR,E192x\t\t; Loop until receive flag set\n\t\tMOV     R7,S0RBUF\t\t; R7 = received byte\n\t\tBCLR    S0RIR\t\t\t; Clear receive flag\n\t\tRET\nE192\tENDP\n\n; Receive a byte in R7. R6: 0 if success, 1 if timeout.\nE1A2\tPROC NEAR\n\t\tMOV\t\t[-R0],R1 ; Push R1\n\t\tMOV     R1,#0FFFFH ; Timeout to receive a byte\n\t\tMOV     R6,#00H ; Success => R6\nE1AA:\tSRVWDT  ; Feed watchdog\n\t\tJB      S0RIR,E1BA ; Jump if byte received\n\t\tCMPD1   R1,#00H\n\t\tJMPR    CC_NZ,E1AA ; Loop is not timeout\n\t\tMOV     R6,#01H ; Timeout => R6\n\t\tJMPR    CC_UC,E1C0\nE1BA:\tMOV     R7,S0RBUF ; Byte => R7\n\t\tBCLR    S0RIR ; Clear receive flag\nE1C0:\tJMPR    CC_UC,E156y ; Pop R1 + RET\nE1A2\tENDP\n\n; Dump EEPROM\nE1C6\tPROC NEAR\n\t\tBSET\tDP2.8 ; P2.8 is an output\n\t\tBSET    P2.8 ; 1 => P2.8\n\t\tBSET    DP2.9 ; P2.9 is an output\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL    Delay256\n\t\tCALL    Delay256\n\t\tCALL    E246 ; 11 01 00 - Start bit\n\t\tMOVB    RL7,#00ACH ; 1010 1100 (Dummy Write)\n\t\tCALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tMOVB    RL7,#00H ; Address 0\n\t\tCALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tCALL    E25C ; 0x 01 11\n\t\tCALL    E246 ; 11 01 00\n\t\tMOVB    RL7,#00ADH ; 1010 1101 (Read)\n\t\tCALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tMOV     R5,#00H\n\t\tMOV     R2,#01FEH ; Count = 512-2\nE20A:\tMOV     R5,#07H\nE20C:\tCALL    Delay256\n\t\tCMPD1   R5,#00H\n\t\tJMPR    CC_NZ,E20C\n\t\tCALL    E2C6 ; Clock (P2.9) 8 bits of P2.8 into RL7 (MSB first)\n\t\tCALL    E298 ; 0x 01 00\n\t\tCALL    XmitRL7\n\t\tCMPD1   R2,#00H\n\t\tJMPR    CC_NZ,E20A\n\t\tCALL    E2C6 ; Clock (P2.9) 8 bits of P2.8 into RL7 (MSB first)\n\t\tCALL    XmitRL7\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tBSET    P2.8 ; 1 => P2.8\n\t\tCALL    Delay7\n\t\tBCLR    P2.9 ; 0 => P2.9\n\t\tCALL    Delay7\n\t\tBCLR    P2.8 ; 0 => P2.8\n\t\tCALL    Delay7\n\t\tJMPR\tCC_UC,E25C ; 0x 01 11\nE1C6\tENDP\n\n; 11 01 00\nE246\tPROC NEAR\n\t\tBSET    P2.8 ; 1 => P2.8\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL\tDelay7\n\t\tBCLR    P2.8 ; 0 => P2.8\n\t\tCALL    Delay7\n\t\tBCLR    P2.9 ; 0 => P2.9\n\t\tCALL    Delay7\n\t\tRET\nE246\tENDP\n\n; 0x 01 11\nE25C\tPROC NEAR\n\t\tBCLR    P2.8 ; 0 => P2.8\n\t\tCALL\tDelay7\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL    Delay7\n\t\tBSET    P2.8 ; 1 => P2.8\n\t\tRET\nE25C\tENDP\n\n; x1 x0\nE26C\tPROC NEAR\n\t\tCALL\tDelay7\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL\tDelay7\n\t\tBCLR    P2.9 ; 0 => P2.9\n\t\tRET\nE26C\tENDP\n\n; Clock (P2.9) one bit of P2.8 into R7.0\nE27A\tPROC NEAR\n\t\tMOV\t\tR7,#00H\n\t\tBCLR    DP2.8 ; P2.8 is an input\n\t\tCALL    Delay7\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL    Delay7\n\t\tBMOV    R7.0,P2.8 ; P2.8 => R7.0\n\t\tBCLR    P2.9 ; 0 => P2.9\n\t\tCALL    Delay7\n\t\tBCLR    P2.8 ; 0 => P2.8\n\t\tBSET    DP2.8 ; P2.8 is an output\n\t\tRET\nE27A\tENDP\n\n; 0x 01 00\nE298\tPROC NEAR\n\t\tBCLR\tP2.8 ; 0 => P2.8\n\t\tCALL    Delay7\n\t\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL    Delay7\n\t\tBCLR    P2.9 ; 0 => P2.9\n\t\tCALL    Delay7\n\t\tRET\nE298\tENDP\n\n; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\nE2AC\tPROC NEAR\n\t\tMOV  \t[-R0],R1 ; Push R1\n\t\tMOV     R1,#07H ; Count = 7\n\t\tSHL     R7,#08H\nE2B2:\tSHL     R7,#01H\n\t\tBMOV    P2.8,C ; High bit of R7 => P2.8\n\t\tCALL    E26C ; x1 x0 (clock)\n\t\tCMPD1   R1,#00H ; Count-- == 0?\n\t\tJMPR    CC_NZ,E2B2 ; Repeat\n\t\tBCLR    P2.8 ; 0 => P2.8\n\t\tJMPR\tCC_UC,PopR1\nE2AC\tENDP\n\n; Clock (P2.9) 8 bits of P2.8 into RL7 (MSB first)\nE2C6\tPROC NEAR\n\t\tMOV  \t[-R0],R1 ; Push R1\n\t\tBCLR    DP2.8 ; P2.8 is an input\n\t\tMOV     R1,#07H ; Count = 7\n\t\tMOV     R7,#00H\nE2CE:\tBSET    P2.9 ; 1 => P2.9\n\t\tCALL    Delay7\n\t\tSHL     R7,#01H\n\t\tBMOV    R7.0,P2.8 ; P2.8 => Low bit of R7\n\t\tBCLR    P2.9 ; 0 => P2.9\n\t\tCALL    Delay7\n\t\tCMPD1   R1,#00H ; Count-- == 0?\n\t\tJMPR    CC_NZ,E2CE ; Repeat\n\t\tBSET    DP2.8 ; P2.8 is an output\n\t\tBCLR    P2.8 ; 0 => P2.8\n\t\tJMPR\tCC_UC,PopR1\nE2C6\tENDP\n\nDelay256\tPROC NEAR\n\t\tMOV  \t[-R0],R3 ; Push R3\n\t\tMOV     R3,#0100H\n\t\tJMPR    CC_UC,DelayLoop\nDelay256\tENDP\n\nDelay7\tPROC NEAR\n\t\tMOV  \t[-R0],R3 ; Push R3\n\t\tMOV     R3,#7\nDelayLoop:\n\t\tSRVWDT  ; Feed watchdog\n\t\tSUB     R3,#1\n\t\tCMP     R3,#00H\n\t\tJMPR    CC_NZ,DelayLoop\n\t\tMOV     R3,[R0+] ; Pop R3\n\t\tRET\nDelay7\tENDP\n\n; Write 1 byte to EEPROM (Page 0)\nE324\tPROC NEAR\nE324x:\n\t\tCALL\tE246 ; 11 01 00 - Start bit\n\t\tMOV     R7,#00ACH ; 1010 1100 (Write)\n\t\tCALL\tE2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tCMP     R7,#0\n\t\tJMPR    CC_NZ,E324x\n\t\tJMPR\tCC_UC,E356y\nE324\tENDP\n\n; Write 1 byte to EEPROM (Page 1)\nE356\tPROC NEAR\nE356x:\n\t\tCALL\tE246 ; 11 01 00 - Start bit\n\t\tMOV     R7,#00AEH ; 1010 1110\n\t\tCALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tCMP     R7,#0\n\t\tJMPR    CC_NZ,E356x\nE356y:\n\t\tCALL    E192 ; Receive a byte in R7\n\t\tCALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tCALL    E192 ; Receive a byte in R7\n\t\tCALL    E2AC ; Clock (P2.9) all bits of RL7 (MSB first) to P2.8\n\t\tCALL    E27A ; Clock (P2.9) one bit of P2.8 into R7.0\n\t\tJMPR\tCC_UC,E25C ; 0x 01 11\nE356\tENDP\n\t\n; Program flash\nE1C4\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     [-R0],R3 ; Push R3\n\t\tMOV     [-R0],R4 ; Push R4\n\t\tMOV     R3,#0E600H ; Buf\n\t\tMOV     R2,#00H\n\t\tMOVB    RL2,[R3+#0002H] ; Address high\n\t\tMOVB    RH1,[R3+#0003H] ; Address medium\n\t\tMOVB    RL1,[R3+#0004H] ; Address low\n\t\tMOV     R4,#00H\n\t\tMOVB    RL4,[R3] ; Count\n\t\tSUB     R4,#4\n\t\tSHR     R4,#01H\n\t\tSUB     R4,#1\n\t\tADD     R3,#5\nE1EA:\tSRVWDT  ; Feed watchdog\n\t\tMOVB    RL7,[R3+]\n\t\tMOVB    RH7,[R3+]\n\t\tCALL    E208 ; Program flash (R1: Address, R7: Data)\n\t\tADD     R1,#2\n\t\tADDC    R2,#0\n\t\tCMPD1   R4,#00H\n\t\tJMPR    CC_NZ,E1EA\nPopR4:\tMOV     R4,[R0+] ; Pop R4\nPopR3:\tMOV     R3,[R0+] ; Pop R3\nPopR2:\tMOV     R2,[R0+] ; Pop R2\nPopR1:\tMOV     R1,[R0+] ; Pop R1\n\t\tRET\nE1C4\tENDP\n\n; Program flash (R1: Address, R7: Data)\nE208\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R7 ; Push R7\n\t\tMOV     R1,#0AAAAH ; Address AAA\n\t\tMOVB    RL7,#00AAH ; Data AA\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#5555H ; Address 555\n\t\tMOVB    RL7,#0055H ; Data 55\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#0AAAAH ; Address AAA\n\t\tMOVB    RL7,#00A0H ; Data A0\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R7,[R0+] ; Pop R7\n\t\tMOV     R1,[R0+] ; Pop R1\n\t\tCALL    E384 ; Write extended data word (source is R7)\n\t\tCALL    E354 ; Wait until operation is complete\n\t\tRET\nE208\tENDP\n\n; Read flash\nE23E\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     [-R0],R3 ; Push R3\n\t\tMOV     [-R0],R4 ; Push R4\n\t\tMOV     [-R0],R5 ; Push R5\n\t\tMOV     R3,#0E600H ; Buf\n\t\tMOV     R2,#00H\n\t\tMOVB    RL2,[R3+#0002H] ; Address high\n\t\tMOVB    RH1,[R3+#0003H] ; Address medium\n\t\tMOVB    RL1,[R3+#0004H] ; Address low\n\t\tMOV     R4,#00H\n\t\tMOVB    RL4,[R3+#0005H] ; Count\n\t\tADDB    RL4,#1\n\t\tMOVB    [R3],RL4\n\t\tADD     R3,#1\n\t\tMOVB    RL7,#0036H\n\t\tMOVB    [R3],RL7\n\t\tADD     R3,#1\n\t\tSUBB    RL4,#1\nE270:\tCALL    E290 ; Read extended data (returned in RL7)\n\t\tMOVB    [R3],RL7\n\t\tADD     R3,#1\n\t\tADD     R1,#1\n\t\tADDC    R2,#0\n\t\tCMPD1   R4,#00H\n\t\tJMPR    CC_NZ,E270\n\t\tCALL    E15E ; Send message in buf (E600)\n\t\tMOV     R5,[R0+] ; Pop R5\n\t\tJMPR\tCC_UC,PopR4\nE23E\tENDP\n\n; Read extended data (returned in RL7)\nE290\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tSRVWDT  ; Feed watchdog\n\t\tCALL    E2A4 ; Set Data Page Pointer 0 based on R2\n\t\tAND     R1,#3FFFH\n\t\tMOVB    RL7,[R1]\n\t\tJMPR\tCC_UC,PopR1\nE290\tENDP\n\n; Set Data Page Pointer 0 based on R1, R2\nE2A4\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tSHL     R2,#02H\n\t\tAND     R2,#00FCH\n\t\tROL     R1,#02H\n\t\tAND     R1,#3\n\t\tOR      R2,R1\n\t\tMOV     DPP0,R2 ; R2 => Data Page Pointer 0\n\t\tJMPR\tCC_UC,PopR2\nE2A4\tENDP\n\n; Erase flash\nE2E6\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     [-R0],R3 ; Push R3\n\t\tMOV     [-R0],R7 ; Push R7\n\t\tMOV     R3,#0E600H ; Buf\n\t\tMOV     R2,#0020H\n\t\tMOV     R1,#0AAAAH ; Address AAA\n\t\tMOVB    RL7,#00AAH ; Data AA\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#5555H ; Address 555\n\t\tMOVB    RL7,#0055H ; Data 55\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#0AAAAH ; Address AAA\n\t\tMOVB    RL7,#0080H ; Data 80\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#0AAAAH ; Address AAA\n\t\tMOVB    RL7,#00AAH ; Data AA\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#5555H ; Address 555\n\t\tMOVB    RL7,#0055H ; Data 55\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tMOV     R1,#0AAAAH ; Address AAA\n\t\tMOVB    RL7,#0010H ; Data 10\n\t\tCALL    E374 ; Write extended data byte (source is RL7)\n\t\tCALL    Delay256\n\t\tMOVB    RL7,#0080H\n\t\tCALL    E354 ; Wait until erase operation is complete\n\t\tMOV     R7,[R0+] ; Pop R7\n\t\tJMPR\tCC_UC,PopR3\nE2E6\tENDP\n\t\n; Wait until operation is complete\nE354\tPROC NEAR\n\t\tMOV     [-R0],R2 ; Push R2\n\t\tMOV     [-R0],R7 ; Push R7\n\t\tSUB     R2,#0018H\n\t\tMOVB    RH7,RL7\n\t\tANDB    RH7,#0080H\nE362:\tCALL    E290 ; Read extended data (returned in RL7)\n\t\tANDB    RL7,#0080H\n\t\tCMPB    RL7,RH7\n\t\tJMPR    CC_NZ,E362\n\t\tMOV     R7,[R0+] ; Pop R7\n\t\tMOV     R2,[R0+] ; Pop R2\n\t\tRET\nE354\tENDP\n\n; Write extended data byte (source is RL7)\nE374\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tCALL    E2A4 ; Set Data Page Pointer 0 based on R2\n\t\tAND     R1,#3FFFH\n\t\tMOVB    [R1],RL7\n\t\tJMPR\tCC_UC,E384x ; Pop R1 + RET\nE374\tENDP\n\t\n; Write extended data word (source is R7)\nE384\tPROC NEAR\n\t\tMOV     [-R0],R1 ; Push R1\n\t\tCALL    E2A4 ; Set Data Page Pointer 0 based on R2\n\t\tAND     R1,#3FFFH\n\t\tMOV     [R1],R7\nE384x:\tMOV     R1,[R0+] ; Pop R1\n\t\tRET\nE384\tENDP\n\nRoutineEnd:\n\nAll\t\tENDS\n\nEND\n"
  },
  {
    "path": "Interface/FtdiInterface.cs",
    "content": "﻿using System;\nusing System.IO.Ports;\nusing System.Reflection;\nusing System.Runtime.InteropServices;\n\nnamespace BitFab.KW1281Test.Interface\n{\n    internal class FtdiInterface : IInterface \n    {\n        private readonly FT _ft;\n        private IntPtr _handle = IntPtr.Zero;\n        private readonly byte[] _buf = new byte[1];\n\n        public FtdiInterface(string serialNumber, int baudRate)\n        {\n            _ft = new FT();\n\n            var status = _ft.Open(\n                serialNumber, FT.OpenExFlags.BySerialNumber, out _handle);\n            FT.AssertOk(status);\n\n            status = _ft.SetBaudRate(_handle, (uint)baudRate);\n            FT.AssertOk(status);\n\n            status = _ft.SetDataCharacteristics(\n                _handle,\n                FT.Bits.Eight,\n                FT.StopBits.One,\n                FT.Parity.None);\n            FT.AssertOk(status);\n\n            status = _ft.SetFlowControl(\n                _handle,\n                FT.FlowControl.None, 0, 0);\n            FT.AssertOk(status);\n\n            SetRts(false);\n            SetDtr(true);\n\n            _readTimeout = _writeTimeout = ((IInterface)this).DefaultTimeoutMilliseconds;\n            ReadTimeout = _readTimeout; // Also sets the write timeout\n\n            // Should allow faster response times for small packets\n            status = _ft.SetLatencyTimer(_handle, 2);\n            FT.AssertOk(status);\n        }\n\n        public void Dispose()\n        {\n            if (_handle != IntPtr.Zero)\n            {\n                SetDtr(false);\n\n                var status = _ft.Close(_handle);\n                _handle = IntPtr.Zero;\n                FT.AssertOk(status);\n            }\n\n            _ft.Dispose();\n        }\n\n        public byte ReadByte()\n        {\n            var status = _ft.Read(_handle, _buf, 1, out uint countOfBytesRead);\n            FT.AssertOk(status);\n            if (countOfBytesRead != 1)\n            {\n                throw new TimeoutException(\"Read timed out\");\n            }\n\n            var b = _buf[0];\n            return b;\n        }\n\n        /// <summary>\n        /// Write a byte to the interface but do not read/discard its echo.\n        /// </summary>\n        public void WriteByteRaw(byte b)\n        {\n            _buf[0] = b;\n            var status = _ft.Write(_handle, _buf, 1, out uint countOfBytesWritten);\n            FT.AssertOk(status);\n            if (countOfBytesWritten != 1)\n            {\n                throw new InvalidOperationException(\n                    $\"Expected to write 1 byte but wrote {countOfBytesWritten} bytes\");\n            }\n        }\n\n        public void SetBreak(bool on)\n        {\n            FT.Status status;\n            if (on)\n            {\n                status = _ft.SetBreakOn(_handle);\n            }\n            else\n            {\n                status = _ft.SetBreakOff(_handle);\n            }\n            FT.AssertOk(status);\n        }\n\n        public void ClearReceiveBuffer()\n        {\n            var status = _ft.Purge(_handle, FT.PurgeMask.RX);\n            FT.AssertOk(status);\n        }\n\n        public void SetBaudRate(int baudRate)\n        {\n            var status = _ft.SetBaudRate(_handle, (uint)baudRate);\n            FT.AssertOk(status);\n        }\n\n        public void SetParity(Parity parity)\n        {\n            var ftParity = parity switch\n            {\n                Parity.None => FT.Parity.None,\n                Parity.Even => FT.Parity.Even,\n                Parity.Odd => FT.Parity.Odd,\n                Parity.Mark => FT.Parity.Mark,\n                Parity.Space => FT.Parity.Space,\n                _ => throw new ArgumentException($\"Unsupported parity: {parity}\", nameof(parity))\n            };\n\n            var status = _ft.SetDataCharacteristics(\n                _handle,\n                FT.Bits.Eight,\n                FT.StopBits.One, \n                ftParity);\n            FT.AssertOk(status);\n        }\n\n        public void SetDtr(bool on)\n        {\n            FT.Status status;\n            if (on)\n            {\n                status = _ft.SetDtr(_handle);\n            }\n            else\n            {\n                status = _ft.ClrDtr(_handle);\n            }\n            FT.AssertOk(status);\n        }\n\n        public void SetRts(bool on)\n        {\n            FT.Status status;\n            if (on)\n            {\n                status = _ft.SetRts(_handle);\n            }\n            else\n            {\n                status = _ft.ClrRts(_handle);\n            }\n            FT.AssertOk(status);\n        }\n\n        private int _readTimeout;\n        \n        public int ReadTimeout\n        {\n            get => _readTimeout;\n\n            set\n            {\n                var status = _ft.SetTimeouts(\n                    _handle,\n                    (uint)value,\n                    (uint)WriteTimeout);\n                FT.AssertOk(status);\n                _readTimeout = value;\n            }\n        }\n\n        private int _writeTimeout;\n        \n        public int WriteTimeout\n        {\n            get => _writeTimeout;\n            set\n            {\n                var status = _ft.SetTimeouts(\n                    _handle,\n                    (uint)ReadTimeout,\n                    (uint)value);\n                FT.AssertOk(status);\n                _writeTimeout = value;\n            }\n        }\n    }\n\n    class FT : IDisposable\n    {\n        private IntPtr _d2xx = IntPtr.Zero;\n\n        // Delegates used to call into the FTID D2xx DLL\n#pragma warning disable CS0649\n        private readonly FTDll.SetVidPid _setVidPid;\n        private readonly FTDll.OpenBySerialNumber _openBySerialNumber;\n        private readonly FTDll.Close _close;\n        private readonly FTDll.SetBaudRate _setBaudRate;\n        private readonly FTDll.SetDataCharacteristics _setDataCharacteristics;\n        private readonly FTDll.SetFlowControl _setFlowControl;\n        private readonly FTDll.SetDtr _setDtr;\n        private readonly FTDll.ClrDtr _clrDtr;\n        private readonly FTDll.SetRts _setRts;\n        private readonly FTDll.ClrRts _clrRts;\n        private readonly FTDll.SetTimeouts _setTimeouts;\n        private readonly FTDll.SetLatencyTimer _setLatencyTimer;\n        private readonly FTDll.Purge _purge;\n        private readonly FTDll.SetBreakOn _setBreakOn;\n        private readonly FTDll.SetBreakOff _setBreakOff;\n        private readonly FTDll.Read _read;\n        private readonly FTDll.Write _write;\n#pragma warning restore CS0649\n\n        public FT()\n        {\n            string libName;\n            bool isMacOs = false;\n            bool isLinux = false;\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n            {\n                libName = \"libftd2xx.dylib\";\n                isMacOs = true;\n            }\n            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n            {\n                libName = \"ftd2xx.so\";\n                isLinux = true;\n            }\n            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                libName = Environment.Is64BitProcess ? \"ftd2xx64.dll\" : \"ftd2xx.dll\";\n            }\n            else\n            {\n                throw new InvalidOperationException($\"Unknown OS: {RuntimeInformation.OSDescription}\");\n            }\n\n            _d2xx = NativeLibrary.Load(\n                libName, typeof(FT).Assembly, DllImportSearchPath.SafeDirectories);\n\n            InitDelegate(nameof(_openBySerialNumber), out _openBySerialNumber);\n            InitDelegate(nameof(_close), out _close);\n            InitDelegate(nameof(_setBaudRate), out _setBaudRate);\n            InitDelegate(nameof(_setDataCharacteristics), out _setDataCharacteristics);\n            InitDelegate(nameof(_setFlowControl), out _setFlowControl);\n            InitDelegate(nameof(_setDtr), out _setDtr);\n            InitDelegate(nameof(_clrDtr), out _clrDtr);\n            InitDelegate(nameof(_setRts), out _setRts);\n            InitDelegate(nameof(_clrRts), out _clrRts);\n            InitDelegate(nameof(_setTimeouts), out _setTimeouts);\n            InitDelegate(nameof(_setLatencyTimer), out _setLatencyTimer);\n            InitDelegate(nameof(_purge), out _purge);\n            InitDelegate(nameof(_setBreakOn), out _setBreakOn);\n            InitDelegate(nameof(_setBreakOff), out _setBreakOff);\n            InitDelegate(nameof(_read), out _read);\n            InitDelegate(nameof(_write), out _write);\n            if (isMacOs || isLinux)\n            {\n                InitDelegate(nameof(_setVidPid), out _setVidPid);\n            }\n            else\n            {\n                _setVidPid = (uint vid, uint pid) => Status.Ok;\n            }\n\n            if (isMacOs || isLinux)\n            {\n                var vidStr = Environment.GetEnvironmentVariable(\"FTDI_VID\");\n                var pidStr = Environment.GetEnvironmentVariable(\"FTDI_PID\");\n                if (!string.IsNullOrEmpty(vidStr) && !string.IsNullOrEmpty(pidStr))\n                {\n                    var vid = Utils.ParseUint(vidStr);\n                    var pid = Utils.ParseUint(pidStr);\n                    Log.WriteLine($\"Setting FTDI VID=0x{vid:X4}, PID=0x{pid:X4}\");\n                    var status = SetVidPid(vid, pid);\n                    AssertOk(status);\n                }\n            }\n        }\n\n        private void InitDelegate<T>(string fieldName, out T delegateVal) where T : Delegate\n        {\n            var symbolNameAttribute = typeof(T).GetCustomAttribute<SymbolNameAttribute>();\n            if (symbolNameAttribute == null)\n            {\n                throw new InvalidOperationException(\n                    $\"Type {typeof(T)} is missing required SymbolName attribute.\");\n            }\n            var nativeMethodName = symbolNameAttribute.Name;\n            var export = NativeLibrary.GetExport(_d2xx, nativeMethodName);\n            delegateVal = Marshal.GetDelegateForFunctionPointer<T>(export);\n        }\n\n        public void Dispose()\n        {\n            if (_d2xx != IntPtr.Zero)\n            {\n                NativeLibrary.Free(_d2xx);\n                _d2xx = IntPtr.Zero;\n            }\n        }\n\n        public static void AssertOk(FT.Status status)\n        {\n            if (status != FT.Status.Ok)\n            {\n                throw new InvalidOperationException(\n                    $\"D2xx library returned {status} instead of Ok\");\n            }\n        }\n\n        public Status SetVidPid(\n            uint vid,\n            uint pid)\n        {\n            return _setVidPid(vid, pid);\n        }\n\n        public Status Open(\n            string serialNumber,\n            OpenExFlags flags,\n            out IntPtr handle)\n        {\n            return _openBySerialNumber(serialNumber, flags, out handle);\n        }\n\n        public Status Close(\n            IntPtr handle)\n        {\n            return _close(handle);\n        }\n\n        public Status SetBaudRate(\n            IntPtr handle,\n            uint baudRate)\n        {\n            return _setBaudRate(handle, baudRate);\n        }\n\n        public Status SetDataCharacteristics(\n            IntPtr handle,\n            Bits wordLength,\n            StopBits stopBits,\n            Parity parity)\n        {\n            return _setDataCharacteristics(handle, wordLength, stopBits, parity);\n        }\n\n        public Status SetFlowControl(\n            IntPtr handle,\n            FlowControl flowControl,\n            byte xonChar,\n            byte xoffChar)\n        {\n            return _setFlowControl(handle, flowControl, xonChar, xoffChar);\n        }\n\n        public Status SetDtr(\n            IntPtr handle)\n        {\n            return _setDtr(handle);\n        }\n\n        public Status ClrDtr(\n            IntPtr handle)\n        {\n            return _clrDtr(handle);\n        }\n\n        public Status SetRts(\n            IntPtr handle)\n        {\n            return _setRts(handle);\n        }\n\n        public Status ClrRts(\n            IntPtr handle)\n        {\n            return _clrRts(handle);\n        }\n\n        public Status SetTimeouts(\n            IntPtr handle,\n            uint readTimeoutMS,\n            uint writeTimeoutMS)\n        {\n            return _setTimeouts(handle, readTimeoutMS, writeTimeoutMS);\n        }\n\n        public Status SetLatencyTimer(\n            IntPtr handle,\n            byte timerMS)\n        {\n            return _setLatencyTimer(handle, timerMS);\n        }\n\n        public Status Purge(\n            IntPtr handle,\n            PurgeMask mask)\n        {\n            return _purge(handle, mask);\n        }\n\n        public Status SetBreakOn(\n            IntPtr handle)\n        {\n            return _setBreakOn(handle);\n        }\n\n        public Status SetBreakOff(\n            IntPtr handle)\n        {\n            return _setBreakOff(handle);\n        }\n\n        public Status Read(\n            IntPtr handle,\n            byte[] buffer,\n            uint countOfBytesToRead,\n            out uint countOfBytesRead)\n        {\n            return _read(handle, buffer, countOfBytesToRead, out countOfBytesRead);\n        }\n\n        public Status Write(\n            IntPtr handle,\n            byte[] buffer,\n            uint countOfBytesToWrite,\n            out uint countOfBytesWritten)\n        {\n            return _write(handle, buffer, countOfBytesToWrite, out countOfBytesWritten);\n        }\n\n        public enum Status : uint\n        {\n            Ok = 0,\n            InvalidHandle,\n            DeviceNotFound,\n            DeviceNotOpened,\n            IOError,\n            insufficient_resources,\n            InvalidParameter,\n            InvalidBaudRate,\n            DeviceNotOpenedForErase,\n            DeviceNotOpenedForWrite,\n            FailedToWriteDevice,\n            EepromReadFailed,\n            EepromWriteFailed,\n            EepromEraseFailed,\n            EepromNotPresent,\n            EepromNotProgrammed,\n            InvalidArgs,\n            NotSupported,\n            OtherError,\n            DeviceListNotReady,\n        };\n\n        [Flags]\n        public enum OpenExFlags : uint\n        {\n            BySerialNumber = 1,\n            ByDescription = 2,\n            ByLocation = 4\n        };\n\n        public enum Bits : byte\n        {\n            Eight = 8,\n            Seven = 7\n        };\n\n        public enum StopBits : byte\n        {\n            One = 0,\n            Two = 2\n        };\n\n        public enum Parity : byte\n        {\n            None = 0,\n            Odd = 1,\n            Even = 2,\n            Mark = 3,\n            Space = 4\n        };\n\n        public enum FlowControl : ushort\n        {\n            None = 0x0000,\n            RtsCts = 0x0100,\n            DtrDsr = 0x0200,\n            XonXoff = 0x0400\n        };\n\n        [Flags]\n        public enum PurgeMask : uint\n        {\n            RX = 1,\n            TX = 2\n        };\n    }\n\n    [AttributeUsage(AttributeTargets.Delegate)]\n    internal class SymbolNameAttribute : Attribute\n    {\n        public SymbolNameAttribute(string name)\n        {\n            Name = name;\n        }\n\n        public string Name { get; private set; }\n    }\n\n    static class FTDll\n    {\n        [SymbolName(\"FT_SetVIDPID\")]\n        public delegate FT.Status SetVidPid(\n            uint vid, uint pid);\n\n        [SymbolName(\"FT_OpenEx\")]\n        public delegate FT.Status OpenBySerialNumber(\n            [MarshalAs(UnmanagedType.LPStr)] string serialNumber,\n            FT.OpenExFlags flags,\n            out IntPtr handle);\n\n        [SymbolName(\"FT_Close\")]\n        public delegate FT.Status Close(\n            IntPtr handle);\n\n        [SymbolName(\"FT_SetBaudRate\")]\n        public delegate FT.Status SetBaudRate(\n            IntPtr handle,\n            uint baudRate);\n\n        [SymbolName(\"FT_SetDataCharacteristics\")]\n        public delegate FT.Status SetDataCharacteristics(\n            IntPtr handle,\n            FT.Bits wordLength,\n            FT.StopBits stopBits,\n            FT.Parity parity);\n\n        [SymbolName(\"FT_SetFlowControl\")]\n        public delegate FT.Status SetFlowControl(\n            IntPtr handle,\n            FT.FlowControl flowControl,\n            byte xonChar,\n            byte xoffChar);\n\n        [SymbolName(\"FT_SetDtr\")]\n        public delegate FT.Status SetDtr(\n            IntPtr handle);\n\n        [SymbolName(\"FT_ClrDtr\")]\n        public delegate FT.Status ClrDtr(\n            IntPtr handle);\n\n        [SymbolName(\"FT_SetRts\")]\n        public delegate FT.Status SetRts(\n            IntPtr handle);\n\n        [SymbolName(\"FT_ClrRts\")]\n        public delegate FT.Status ClrRts(\n            IntPtr handle);\n\n        [SymbolName(\"FT_SetTimeouts\")]\n        public delegate FT.Status SetTimeouts(\n            IntPtr handle,\n            uint readTimeoutMS,\n            uint writeTimeoutMS);\n\n        [SymbolName(\"FT_SetLatencyTimer\")]\n        public delegate FT.Status SetLatencyTimer(\n            IntPtr handle,\n            byte timerMS);\n\n        [SymbolName(\"FT_Purge\")]\n        public delegate FT.Status Purge(\n            IntPtr handle,\n            FT.PurgeMask mask);\n\n        [SymbolName(\"FT_SetBreakOn\")]\n        public delegate FT.Status SetBreakOn(\n            IntPtr handle);\n\n        [SymbolName(\"FT_SetBreakOff\")]\n        public delegate FT.Status SetBreakOff(\n            IntPtr handle);\n\n        [SymbolName(\"FT_Read\")]\n        public delegate FT.Status Read(\n            IntPtr handle,\n            byte[] buffer,\n            uint countOfBytesToRead,\n            out uint countOfBytesRead);\n\n        [SymbolName(\"FT_Write\")]\n        public delegate FT.Status Write(\n            IntPtr handle,\n            byte[] buffer,\n            uint countOfBytesToWrite,\n            out uint countOfBytesWritten);\n    }\n}\n"
  },
  {
    "path": "Interface/GenericInterface.cs",
    "content": "﻿using System.IO.Ports;\n\nnamespace BitFab.KW1281Test.Interface\n{\n    internal class GenericInterface : IInterface\n    {\n        public GenericInterface(string portName, int baudRate)\n        {\n            var timeout = ((IInterface)this).DefaultTimeoutMilliseconds;\n            _port = new SerialPort(portName)\n            {\n                BaudRate = baudRate,\n                DataBits = 8,\n                Parity = Parity.None,\n                StopBits = StopBits.One,\n                Handshake = Handshake.None,\n                RtsEnable = false,\n                DtrEnable = true,\n                ReadTimeout = timeout,\n                WriteTimeout = timeout\n            };\n\n            _port.Open();\n        }\n\n        public void Dispose()\n        {\n            SetDtr(false);\n            _port.Close();\n        }\n\n        public byte ReadByte()\n        {\n            var b = (byte)_port.ReadByte();\n            return b;\n        }\n\n        public void WriteByteRaw(byte b)\n        {\n            _buf[0] = b;\n            _port.Write(_buf, 0, 1);\n        }\n\n        public void SetBreak(bool on)\n        {\n            _port.BreakState = on;\n        }\n\n        public void ClearReceiveBuffer()\n        {\n            _port.DiscardInBuffer();\n        }\n\n        public void SetBaudRate(int baudRate)\n        {\n            _port.BaudRate = baudRate;\n        }\n\n        public void SetParity(Parity parity)\n        {\n            _port.Parity = parity;\n        }\n\n        public void SetDtr(bool on)\n        {\n            _port.DtrEnable = on;\n        }\n\n        public void SetRts(bool on)\n        {\n            _port.RtsEnable = on;\n        }\n\n        public int ReadTimeout\n        {\n            get => _port.ReadTimeout;\n            set => _port.ReadTimeout = value;\n        }\n\n        public int WriteTimeout\n        {\n            get => _port.WriteTimeout;\n            set => _port.WriteTimeout = value;\n        }\n\n        private readonly SerialPort _port;\n\n        private readonly byte[] _buf = new byte[1];\n    }\n}\n"
  },
  {
    "path": "Interface/IInterface.cs",
    "content": "﻿using System;\nusing System.IO.Ports;\n\nnamespace BitFab.KW1281Test.Interface\n{\n    public interface IInterface : IDisposable\n    {\n        int DefaultTimeoutMilliseconds => (int)TimeSpan.FromSeconds(8).TotalMilliseconds;\n\n        /// <summary>\n        /// Read a byte from the interface.\n        /// </summary>\n        /// <returns>The byte.</returns>\n        byte ReadByte();\n\n        /// <summary>\n        /// Write a byte to the interface but do not read/discard its echo.\n        /// </summary>\n        void WriteByteRaw(byte b);\n\n        void SetBreak(bool on);\n\n        void ClearReceiveBuffer();\n\n        void SetBaudRate(int baudRate);\n\n        void SetParity(Parity parity);\n\n        void SetDtr(bool on);\n\n        void SetRts(bool on);\n\n        int ReadTimeout { get; set; }\n\n        int WriteTimeout { get; set; }\n    }\n}\n"
  },
  {
    "path": "Interface/LinuxInterface.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing System.IO.Ports;\n\nusing tcflag_t = System.UInt32;\nusing cc_t = System.Byte;\nusing speed_t = System.UInt32;\n\nnamespace BitFab.KW1281Test.Interface;\n\npublic class LinuxInterface : IInterface\n{\n    private const uint CBAUD  = 0x100F; // Clear normal baudrates\n    private const uint CBAUDEX = 0x1000;\n    private const uint BOTHER = 0x1000; // Other baudrate\n    private const int IBSHIFT = 16; // Shift from CBAUD to CIBAUD\n    private const uint PARENB = 0x0100; // Enable parity bit\n    private const uint PARODD = 0x0200; // Use odd parity rather than even parity\n\n    private const string libc = \"libc\";\n\n#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time\n\n    // Linux ioctl function\n    [DllImport(libc, SetLastError = true)]\n    private static extern int ioctl(int fd, int request, ref int data);\n\n    [DllImport(libc, SetLastError = true)]\n    private static extern int ioctl(int fd, uint request, IntPtr data);\n\n    // Native method declarations\n    [DllImport(libc)]\n    private static extern int open(string pathname, int flags);\n\n    [DllImport(libc)]\n    private static extern int close(int fd);\n\n    [DllImport(libc, CallingConvention = CallingConvention.StdCall)]\n    private static extern int read(int fd, byte[] buf, int count);\n\n    [DllImport(libc, CallingConvention = CallingConvention.StdCall)]\n    private static extern int write(int fd, byte[] buf, int count);\n\n    [DllImport(libc, CallingConvention = CallingConvention.StdCall)]\n    private static extern int tcflush(int fd, int queue);\n\n#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time\n\n    private const int NCCS = 19;\n\n    // Define the termios structure to interact with terminal I/O settings\n    [StructLayout(LayoutKind.Sequential)]\n    public struct Termios\n    {\n        public tcflag_t c_iflag;    // input mode flags\n        public tcflag_t c_oflag;    // output mode flags\n        public tcflag_t c_cflag;    // control mode flags\n        public tcflag_t c_lflag;    // local mode flags\n        public cc_t c_line;         // line discipline\n\n        [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)]\n        public cc_t[] c_cc;         // control characters\n\n        public speed_t c_ispeed;    // input speed\n        public speed_t c_ospeed;    // output speed\n    }\n\n    private const int _IOC_NRBITS =\t8;\n    private const int _IOC_TYPEBITS\t= 8;\n    private const int _IOC_SIZEBITS = 14;\n\n    private const int _IOC_NRSHIFT = 0;\n    private const int _IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS);\n    private const int _IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS);\n    private const int _IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS);\n\n    private const int _IOC_READ = 2;\n    private const int _IOC_WRITE = 1;\n\n#pragma warning disable IDE1006 // Naming Styles\n    private static uint _IOC(int dir, int type, int nr, int size)\n    {\n        return (uint)((dir << _IOC_DIRSHIFT) |\n            (type << _IOC_TYPESHIFT) |\n            (nr   << _IOC_NRSHIFT) |\n            (size << _IOC_SIZESHIFT));\n    }\n\n    private static int _IOC_TYPECHECK(Type type)\n    {\n        return Marshal.SizeOf(type);\n    }\n\n    private static uint _IOR(int type, int nr, Type size)\n    {\n        return _IOC(_IOC_READ, type, nr, _IOC_TYPECHECK(size));\n    }\n\n    private static uint _IOW(int type, int nr, Type size)\n    {\n        return _IOC(_IOC_WRITE, type, nr, _IOC_TYPECHECK(size));\n    }\n#pragma warning restore IDE1006 // Naming Styles\n\n    private static readonly uint TCGETS2 = _IOR('T', 0x2A, typeof(Termios));\n    private static readonly uint TCSETS2 = _IOW('T', 0x2B, typeof(Termios));\n\n    private const int TIOCSBRK = 0x5427;\n    private const int TIOCCBRK = 0x5428;\n    private const int TIOCM_RTS = 0x004;\n    private const int TIOCMGET = 0x5415;\n    private const int TIOCMSET = 0x5418;\n    private const int TIOCM_DTR = 0x002;\n    private const int O_RDWR = 2;\n    private const int O_NOCTTY = 00000400;\n    private const int TCIFLUSH = 0; // Discard data received but not yet read\n\n    private const int VTIME = 5;\n    private const int VMIN = 6;\n\n    private int _fd = -1;\n\n    private IntPtr _termios;\n\n    public int ReadTimeout { get; set; }\n\n    public int WriteTimeout { get; set; }\n\n    public LinuxInterface(string portName, int baudRate)\n    {\n        _fd = open(portName, O_RDWR | O_NOCTTY);\n        if (_fd == -1)\n        {\n            throw new IOException($\"Failed to open port {portName}\");\n        }\n\n        // Allocate struct and memory\n        _termios = Marshal.AllocHGlobal(Marshal.SizeOf<Termios>());\n\n        var termios = GetTtyConfiguration();\n\n        // Update termio struct with timeouts\n        termios.c_iflag = 0;\n        termios.c_oflag = 0;\n        termios.c_lflag = 0;\n\n        var timeout = ((IInterface)this).DefaultTimeoutMilliseconds;\n        termios.c_cc[VTIME] = (byte)(timeout / 100);\n        termios.c_cc[VMIN] = 0;\n\n        SetTtyConfiguration(termios);\n\n        SetBaudRate(baudRate);\n    }\n\n    private Termios GetTtyConfiguration()\n    {\n        if (ioctl(_fd, TCGETS2, _termios) == -1)\n        {\n            throw new IOException(\"Failed to get the UART configuration\");\n        }\n        var termios = Marshal.PtrToStructure<Termios>(_termios);\n        return termios;\n    }\n\n    private void SetTtyConfiguration(Termios termios)\n    {\n        // Get a C pointer to struct\n        Marshal.StructureToPtr(termios, _termios, fDeleteOld: true);\n\n        // Update configuration\n        if (ioctl(_fd, TCSETS2, _termios) == -1)\n        {\n            throw new IOException(\"Failed to set the UART configuration\");\n        }\n    }\n\n    private readonly byte[] _buffer = new byte[1];\n\n    public byte ReadByte()\n    {\n        // Console.WriteLine(\"XX ReadByte\");\n\n        int bytesRead = read(_fd, _buffer, 1);\n        if (bytesRead != 1)\n        {\n            throw new IOException(\"Failed to read byte from UART\");\n        }\n        return _buffer[0];\n    }\n\n    public void WriteByteRaw(byte b)\n    {\n        _buffer[0] = b;\n        int bytesWritten = write(_fd, _buffer, 1);\n        if (bytesWritten != 1)\n        {\n            throw new IOException(\"Failed to write byte to UART\");\n        }\n    }\n\n    public void SetBreak(bool on)\n    {\n         var iov = on ? TIOCSBRK : TIOCCBRK;\n\n        int data = 0;\n\n        if (ioctl(_fd, iov, ref data) == -1)\n        {\n            throw new IOException(\"Failed to set/clear UART break\");\n        }\n    }\n\n    public void ClearReceiveBuffer()\n    {\n        if (tcflush(_fd, TCIFLUSH) == -1)\n        {\n            throw new IOException(\"Failed to clear the UART receive buffer\");\n        }\n    }\n\n    public void SetBaudRate(int baudRate)\n    {\n        var termios = GetTtyConfiguration();\n\n        // Output speed\n        termios.c_cflag &= ~(CBAUD | CBAUDEX);\n        termios.c_cflag |= BOTHER;\n        termios.c_ospeed = (uint)baudRate;\n\n        // Input speed\n        termios.c_cflag &= ~((CBAUD | CBAUDEX) << IBSHIFT);\n        termios.c_cflag |= (BOTHER << IBSHIFT);\n        termios.c_ispeed = (uint)baudRate;\n\n        SetTtyConfiguration(termios);\n    }\n\n    public void SetParity(Parity parity)\n    {\n        var termios = GetTtyConfiguration();\n\n        // Set parity\n        switch (parity)\n        {\n            case Parity.None:\n                termios.c_cflag &= ~PARENB; // Disable parity\n                break;\n            case Parity.Odd:\n                termios.c_cflag |= PARENB;  // Enable parity\n                termios.c_cflag |= PARODD;  // Set odd parity\n                break;\n            case Parity.Even:\n                termios.c_cflag |= PARENB;  // Enable parity\n                termios.c_cflag &= ~PARODD; // Set even parity\n                break;\n            case Parity.Mark:\n                // Mark parity is not supported on Linux, set as None\n                termios.c_cflag &= ~PARENB; // Disable parity\n                break;\n            case Parity.Space:\n                // Space parity is not supported on Linux, set as None\n                termios.c_cflag &= ~PARENB; // Disable parity\n                break;\n        }\n\n        SetTtyConfiguration(termios);\n    }\n\n    public void SetDtr(bool on)\n    {\n        // Get the current control lines state\n        int controlLinesState = 0;\n\n        if (ioctl(_fd, TIOCMGET, ref controlLinesState) == -1)\n        {\n            throw new IOException(\"Failed to get control lines state.\");\n        }\n\n        // Set DTR flag\n        if (on)\n        {\n            controlLinesState |= TIOCM_DTR;\n        }\n        else\n        {\n            controlLinesState &= ~TIOCM_DTR;\n        }\n\n        // Set the modified control lines state\n        if (ioctl(_fd, TIOCMSET, ref controlLinesState) == -1)\n        {\n            throw new IOException(\"Failed to set DTR\");\n        }\n    }\n\n    public void SetRts(bool on)\n    {\n        // Get the current control lines state\n        int controlLinesState = 0;\n        if (ioctl(_fd, TIOCMGET, ref controlLinesState) == -1)\n        {\n            throw new IOException(\"Failed to get uart line state\");\n        }\n\n        // Set RTS flag\n        if (on)\n            controlLinesState |= TIOCM_RTS;\n        else\n            controlLinesState &= ~TIOCM_RTS;\n\n        // Set the modified control lines state\n        if (ioctl(_fd, TIOCMSET, ref controlLinesState) == -1)\n        {\n            throw new IOException(\"Failed to set RTS\");\n        }\n    }\n\n    public void Dispose()\n    {\n        // Dispose of unmanaged resources.\n        Dispose(true);\n\n        // Suppress finalization.\n        GC.SuppressFinalize(this);\n    }\n\n    private bool _disposed = false;\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (_disposed)\n        {\n            return;\n        }\n\n        if (disposing)\n        {\n            // TODO: Dispose managed state (managed objects).\n        }\n\n        // Free unmanaged resources (unmanaged objects) and override a finalizer below.\n\n        if (_termios != IntPtr.Zero)\n        {\n            Marshal.FreeHGlobal(_termios);\n            _termios = IntPtr.Zero;\n        }\n        if (_fd != -1)\n        {\n            _ = close(_fd);\n            _fd = -1;\n        }\n\n        // TODO: Set large fields to null.\n\n        _disposed = true;\n    }\n}\n"
  },
  {
    "path": "KW1281Dialog.cs",
    "content": "﻿using BitFab.KW1281Test.Blocks;\nusing BitFab.KW1281Test.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace BitFab.KW1281Test;\n\n/// <summary>\n/// Manages a dialog with a VW controller using the KW1281 protocol.\n/// </summary>\ninternal interface IKW1281Dialog\n{\n    ControllerInfo Connect();\n\n    void EndCommunication();\n\n    void SetDisconnected();\n\n    List<Block> Login(ushort code, int workshopCode);\n\n    List<ControllerIdent> ReadIdent();\n\n    /// <summary>\n    /// Corresponds to VDS-Pro function 19\n    /// </summary>\n    List<byte>? ReadEeprom(ushort address, byte count);\n\n    bool WriteEeprom(ushort address, List<byte> values);\n\n    /// <summary>\n    /// Corresponds to VDS-Pro functions 21 and 22\n    /// </summary>\n    List<byte>? ReadRomEeprom(ushort address, byte count);\n\n    /// <summary>\n    /// Corresponds to VDS-Pro functions 20 and 25\n    /// </summary>\n    List<byte>? ReadRam(ushort address, byte count);\n\n    bool AdaptationRead(byte channelNumber);\n\n    bool AdaptationTest(byte channelNumber, ushort channelValue);\n\n    bool AdaptationSave(byte channelNumber, ushort channelValue, int workshopCode);\n\n    void SendBlock(List<byte> blockBytes);\n\n    List<Block> ReceiveBlocks();\n\n    List<byte>? ReadCcmRom(byte seg, byte msb, byte lsb, byte count);\n\n    /// <summary>\n    /// Keep the dialog alive by sending an ACK and receiving a response.\n    /// </summary>\n    void KeepAlive();\n\n    ActuatorTestResponseBlock? ActuatorTest(byte value);\n\n    List<FaultCode>? ReadFaultCodes();\n\n    /// <summary>\n    /// Clear all of the controllers fault codes.\n    /// </summary>\n    /// <param name=\"controllerAddress\"></param>\n    /// <returns>Any remaining fault codes.</returns>\n    List<FaultCode>? ClearFaultCodes(int controllerAddress);\n\n    /// <summary>\n    /// Set the controller's software coding and workshop code.\n    /// </summary>\n    /// <param name=\"controllerAddress\"></param>\n    /// <param name=\"softwareCoding\"></param>\n    /// <param name=\"workshopCode\"></param>\n    /// <returns>True if successful.</returns>\n    bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int workshopCode);\n\n    bool GroupRead(byte groupNumber, bool useBasicSetting = false);\n\n    List<byte> ReadSecureImmoAccess(List<byte> blockBytes);\n\n    public IKwpCommon KwpCommon { get; }\n    Block ReceiveBlock();\n}\n\ninternal class KW1281Dialog : IKW1281Dialog\n{\n    public ControllerInfo Connect()\n    {\n        _isConnected = true;\n        var blocks = ReceiveBlocks();\n        return new ControllerInfo(blocks.Where(b => !b.IsAckNak));\n    }\n\n    public List<Block> Login(ushort code, int workshopCode)\n    {\n        Log.WriteLine(\"Sending Login block\");\n        SendBlock(\n        [\n            (byte)BlockTitle.Login,\n            (byte)(code >> 8),\n            (byte)(code & 0xFF),\n            (byte)(workshopCode >> 16),\n            (byte)((workshopCode >> 8) & 0xFF),\n            (byte)(workshopCode & 0xFF)\n        ]);\n\n        return ReceiveBlocks();\n    }\n\n    public List<ControllerIdent> ReadIdent()\n    {\n        var idents = new List<ControllerIdent>();\n        bool moreAvailable;\n        do\n        {\n            Log.WriteLine(\"Sending ReadIdent block\");\n\n            SendBlock(new List<byte> { (byte)BlockTitle.ReadIdent });\n\n            var blocks = ReceiveBlocks();\n            var ident = new ControllerIdent(blocks.Where(b => !b.IsAckNak));\n            idents.Add(ident);\n\n            moreAvailable = blocks\n                .OfType<AsciiDataBlock>()\n                .Any(b => b.MoreDataAvailable);\n        } while (moreAvailable);\n\n        return idents;\n    }\n\n    /// <summary>\n    /// Reads a range of bytes from the EEPROM.\n    /// </summary>\n    /// <param name=\"address\"></param>\n    /// <param name=\"count\"></param>\n    /// <returns>The bytes or null if the bytes could not be read</returns>\n    public List<byte>? ReadEeprom(ushort address, byte count)\n    {\n        Log.WriteLine($\"Sending ReadEeprom block (Address: ${address:X4}, Count: ${count:X2})\");\n        SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.ReadEeprom,\n            count,\n            (byte)(address >> 8),\n            (byte)(address & 0xFF)\n        });\n        var blocks = ReceiveBlocks();\n\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            // Permissions issue\n            return null;\n        }\n\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            throw new InvalidOperationException($\"ReadEeprom returned {blocks.Count} blocks instead of 1\");\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    /// <summary>\n    /// Reads a range of bytes from the RAM.\n    /// </summary>\n    /// <param name=\"address\"></param>\n    /// <param name=\"count\"></param>\n    /// <returns>The bytes or null if the bytes could not be read</returns>\n    public List<byte>? ReadRam(ushort address, byte count)\n    {\n        Log.WriteLine($\"Sending ReadRam block (Address: ${address:X4}, Count: ${count:X2})\");\n        SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.ReadRam,\n            count,\n            (byte)(address >> 8),\n            (byte)(address & 0xFF)\n        });\n        var blocks = ReceiveBlocks();\n\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            // Permissions issue\n            return null;\n        }\n\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            throw new InvalidOperationException($\"ReadEeprom returned {blocks.Count} blocks instead of 1\");\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    /// <summary>\n    /// Reads a range of bytes from the CCM ROM.\n    /// </summary>\n    /// <param name=\"seg\">0-15</param>\n    /// <param name=\"msb\">0-15</param>\n    /// <param name=\"lsb\">0-255</param>\n    /// <param name=\"count\">8(-12?)</param>\n    /// <returns>The bytes or null if the bytes could not be read</returns>\n    public List<byte>? ReadCcmRom(byte seg, byte msb, byte lsb, byte count)\n    {\n        Log.WriteLine(\n            $\"Sending ReadEeprom block (Address: ${seg:X2}{msb:X2}{lsb:X2}, Count: ${count:X2})\");\n        var block = new List<byte>\n        {\n            (byte)BlockTitle.ReadEeprom,\n            count,\n            msb,\n            lsb,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            (byte)(seg << 4)\n        };\n        Log.WriteLine($\"SEND {Utils.Dump(block)}\");\n        SendBlock(block);\n        var blocks = ReceiveBlocks();\n#if true\n        foreach (var b in blocks)\n        {\n            Log.WriteLine($\"Received:{Utils.Dump(b.Bytes)}\");\n        }\n#endif\n\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            // Log.WriteLine($\"RECV {Utils.Dump(blocks.First().Bytes)}\");\n            // Permissions issue\n            return null;\n        }\n\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            throw new InvalidOperationException($\"ReadEeprom returned {blocks.Count} blocks instead of 1\");\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    public bool WriteEeprom(ushort address, List<byte> values)\n    {\n        Log.WriteLine($\"Sending WriteEeprom block (Address: ${address:X4}, Values: {Utils.DumpBytes(values)}\");\n\n        byte count = (byte)values.Count;\n        var sendBody = new List<byte>\n        {\n            (byte)BlockTitle.WriteEeprom,\n            count,\n            (byte)(address >> 8),\n            (byte)(address & 0xFF),\n        };\n        sendBody.AddRange(values);\n\n        SendBlock(sendBody.ToList());\n        var blocks = ReceiveBlocks();\n\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            // Permissions issue\n            Log.WriteLine(\"WriteEeprom failed\");\n            return false;\n        }\n\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            Log.WriteLine($\"WriteEeprom returned {blocks.Count} blocks instead of 1\");\n            return false;\n        }\n\n        var block = blocks[0];\n        if (block is not WriteEepromResponseBlock)\n        {\n            Log.WriteLine($\"Expected WriteEepromResponseBlock but got {block.GetType()}\");\n            return false;\n        }\n\n        if (!Enumerable.SequenceEqual(block.Body, sendBody.Skip(1).Take(4)))\n        {\n            Log.WriteLine(\"WriteEepromResponseBlock body does not match WriteEepromBlock\");\n            return false;\n        }\n\n        return true;\n    }\n\n    public List<byte>? ReadRomEeprom(ushort address, byte count)\n    {\n        Log.WriteLine($\"Sending ReadRomEeprom block (Address: ${address:X4}, Count: ${count:X2})\");\n        SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.ReadRomEeprom,\n            count,\n            (byte)(address >> 8),\n            (byte)(address & 0xFF)\n        });\n        var blocks = ReceiveBlocks();\n\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            // Permissions issue\n            return null;\n        }\n\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            throw new InvalidOperationException($\"ReadRomEeprom returned {blocks.Count} blocks instead of 1\");\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    public void EndCommunication()\n    {\n        if (_isConnected)\n        {\n            Log.WriteLine(\"Sending EndCommunication block\");\n            SendBlock(new List<byte> { (byte)BlockTitle.End });\n            _isConnected = false;\n        }\n    }\n\n    public void SetDisconnected()\n    {\n        _isConnected = false;\n        _blockCounter = null;\n    }\n\n    public void SendBlock(List<byte> blockBytes)\n    {\n        Thread.Sleep(25); // For better support of 4D0919035AJ\n\n        var blockLength = (byte)(blockBytes.Count + 2);\n\n        blockBytes.Insert(0, _blockCounter!.Value);\n        _blockCounter++;\n\n        blockBytes.Insert(0, blockLength);\n\n        Thread.Sleep(TimeInterval.R6);\n\n        foreach (var b in blockBytes)\n        {\n            WriteByteAndReadAck(b);\n            Thread.Sleep(TimeInterval.R6);\n        }\n\n        KwpCommon.WriteByte(0x03); // Block end, does not get ACK'd\n    }\n\n    public List<Block> ReceiveBlocks()\n    {\n        var blocks = new List<Block>();\n\n        try\n        {\n            while (true)\n            {\n                var block = ReceiveBlock();\n                blocks.Add(block); // TODO: Maybe don't add the block if it's an Ack\n                if (block is AckBlock || block is NakBlock)\n                {\n                    break;\n                }\n                SendAckBlock();\n            }\n        }\n        catch (Exception ex)\n        {\n            Log.WriteLine($\"Error receiving blocks: {ex.Message}\");\n            if (blocks.Count > 0)\n            {\n                Log.WriteLine(\"Blocks received:\");\n                foreach (var block in blocks)\n                {\n                    Log.WriteLine($\"Block: {Utils.DumpBytes(block.Bytes)}\");\n                }\n            }\n            throw;\n        }\n\n        return blocks;\n    }\n\n    private void WriteByteAndReadAck(byte b)\n    {\n        KwpCommon.WriteByte(b);\n        KwpCommon.ReadComplement(b);\n    }\n\n    public Block ReceiveBlock()\n    {\n        var blockBytes = new List<byte>();\n\n        try\n        {\n            var blockLength = ReadAndAckByteFirst();\n            blockBytes.Add(blockLength);\n\n            var blockCounter = ReadBlockCounter();\n            blockBytes.Add(blockCounter);\n\n            var blockTitle = ReadAndAckByte();\n            blockBytes.Add(blockTitle);\n\n            for (var i = 0; i < blockLength - 3; i++)\n            {\n                var b = ReadAndAckByte();\n                blockBytes.Add(b);\n            }\n\n            var blockEnd = KwpCommon.ReadByte();\n            blockBytes.Add(blockEnd);\n            if (blockEnd != 0x03)\n            {\n                throw new InvalidOperationException(\n                    $\"Received block end ${blockEnd:X2} but expected $03. Block bytes: {Utils.Dump(blockBytes)}\");\n            }\n\n            return (BlockTitle)blockTitle switch\n            {\n                BlockTitle.ACK => new AckBlock(blockBytes),\n                BlockTitle.GroupReadResponseWithText => new GroupReadResponseWithTextBlock(blockBytes),\n                BlockTitle.ActuatorTestResponse => new ActuatorTestResponseBlock(blockBytes),\n                BlockTitle.AsciiData =>\n                    blockBytes[3] == 0x00 ? new CodingWscBlock(blockBytes) : new AsciiDataBlock(blockBytes),\n                BlockTitle.Custom => new CustomBlock(blockBytes),\n                BlockTitle.NAK => new NakBlock(blockBytes),\n                BlockTitle.ReadEepromResponse => new ReadEepromResponseBlock(blockBytes),\n                BlockTitle.FaultCodesResponse => new FaultCodesBlock(blockBytes),\n                BlockTitle.ReadRomEepromResponse => new ReadRomEepromResponse(blockBytes),\n                BlockTitle.WriteEepromResponse => new WriteEepromResponseBlock(blockBytes),\n                BlockTitle.AdaptationResponse => new AdaptationResponseBlock(blockBytes),\n                BlockTitle.GroupReadResponse => new GroupReadResponseBlock(blockBytes),\n                BlockTitle.RawDataReadResponse => new RawDataReadResponseBlock(blockBytes),\n                BlockTitle.SecurityAccessMode2 => new SecurityAccessMode2Block(blockBytes),\n                _ => new UnknownBlock(blockBytes),\n            };\n        }\n        catch (Exception ex)\n        {\n            Log.WriteLine($\"Error receiving block: {ex.Message}\");\n            Log.WriteLine($\"Partial block: {Utils.DumpBytes(blockBytes)}\");\n            if (ex is TimeoutException)\n            {\n                Log.WriteLine($\"Read timeout: {KwpCommon.Interface.ReadTimeout}\");\n                Log.WriteLine($\"Write timeout: {KwpCommon.Interface.WriteTimeout}\");\n            }\n            throw;\n        }\n    }\n\n    private void SendAckBlock()\n    {\n        var blockBytes = new List<byte> { (byte)BlockTitle.ACK };\n        SendBlock(blockBytes);\n    }\n\n    private byte ReadBlockCounter()\n    {\n        var blockCounter = ReadAndAckByte();\n        if (!_blockCounter.HasValue)\n        {\n            // First block\n            _blockCounter = blockCounter;\n        }\n        else if (blockCounter != _blockCounter)\n        {\n            throw new InvalidOperationException(\n                $\"Received block counter ${blockCounter:X2} but expected ${_blockCounter:X2}\");\n        }\n        _blockCounter++;\n        return blockCounter;\n    }\n\n    private byte ReadAndAckByte()\n    {\n        var b = KwpCommon.ReadByte();\n        Thread.Sleep(TimeInterval.R6);\n        var complement = (byte)~b;\n        KwpCommon.WriteByte(complement);\n        return b;\n    }\n\n    /// <summary>\n    /// https://github.com/gmenounos/kw1281test/issues/93\n    /// </summary>\n    private byte ReadAndAckByteFirst(int count = 0)\n    {\n        if (count > 5)\n        {\n            throw new InvalidOperationException(\n                $\"Cannot sync with {count} repeated attempts.\");\n        }\n        var b = KwpCommon.ReadByte();\n        if (b == 0x55)\n        {\n            var keywordLsb = KwpCommon.ReadByte();\n            var keywordMsb = KwpCommon.ReadByte();\n            var complement = (byte)~keywordMsb;\n            BusyWait.Delay(25);\n            KwpCommon.WriteByte(complement);\n            Log.WriteLine($\"Warning. Sync repeated.\");\n            return ReadAndAckByteFirst(count);\n        }\n        else\n        {\n            Thread.Sleep(TimeInterval.R6);\n            var complement = (byte)~b;\n            KwpCommon.WriteByte(complement);\n            return b;\n        }\n    }\n\n    public void KeepAlive()\n    {\n        SendAckBlock();\n        var block = ReceiveBlock();\n        if (block is not AckBlock)\n        {\n            throw new InvalidOperationException(\n                $\"Received 0x{block.Title:X2} block but expected ACK\");\n        }\n    }\n\n    public ActuatorTestResponseBlock? ActuatorTest(byte value)\n    {\n        Log.WriteLine($\"Sending actuator test 0x{value:X2} block\");\n        SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.ActuatorTest,\n            value\n        });\n\n        var blocks = ReceiveBlocks();\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            Log.WriteLine($\"ActuatorTest returned {blocks.Count} blocks instead of 1\");\n            return null;\n        }\n\n        var block = blocks[0];\n        if (block is not ActuatorTestResponseBlock)\n        {\n            Log.WriteLine($\"Expected ActuatorTestResponseBlock but got {block.GetType()}\");\n            return null;\n        }\n\n        return (ActuatorTestResponseBlock)block;\n    }\n\n    public List<FaultCode>? ReadFaultCodes()\n    {\n        Log.WriteLine($\"Sending ReadFaultCodes block\");\n        SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.FaultCodesRead\n        });\n\n        var blocks = ReceiveBlocks();\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n\n        var faultCodes = new List<FaultCode>();\n        foreach (var block in blocks)\n        {\n            if (block is not FaultCodesBlock)\n            {\n                Log.WriteLine($\"Expected FaultCodesBlock but got {block.GetType()}\");\n                return null;\n            }\n\n            var faultCodesBlock = (FaultCodesBlock)block;\n            faultCodes.AddRange(faultCodesBlock.FaultCodes);\n        }\n\n        return faultCodes;\n    }\n\n    public List<FaultCode>? ClearFaultCodes(int controllerAddress)\n    {\n        Log.WriteLine($\"Sending ClearFaultCodes block\");\n        SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.FaultCodesDelete\n        });\n\n        var blocks = ReceiveBlocks();\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n\n        var faultCodes = new List<FaultCode>();\n        foreach (var block in blocks)\n        {\n            if (block is not FaultCodesBlock)\n            {\n                Log.WriteLine($\"Expected FaultCodesBlock but got {block.GetType()}\");\n                return null;\n            }\n\n            var faultCodesBlock = (FaultCodesBlock)block;\n            faultCodes.AddRange(faultCodesBlock.FaultCodes);\n        }\n\n        return faultCodes;\n    }\n\n    public bool SetSoftwareCoding(int controllerAddress, int softwareCoding, int workshopCode)\n    {\n        // Workshop codes > 65535 overflow into the low bit of the software coding\n        var bytes = new List<byte>\n        {\n            (byte)BlockTitle.SoftwareCoding,\n            (byte)((softwareCoding * 2) / 256),\n            (byte)((softwareCoding * 2) % 256),\n            (byte)((workshopCode & 65535) / 256),\n            (byte)(workshopCode % 256)\n        };\n\n        if (workshopCode > 65535)\n        {\n            bytes[2]++;\n        }\n\n        Log.WriteLine($\"Sending SoftwareCoding block\");\n        SendBlock(bytes);\n\n        var blocks = ReceiveBlocks();\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            return false;\n        }\n\n        var controllerInfo = new ControllerInfo(blocks.Where(b => !b.IsAckNak));\n        return\n            controllerInfo.SoftwareCoding == softwareCoding &&\n            controllerInfo.WorkshopCode == workshopCode;\n    }\n\n    public bool AdaptationRead(byte channelNumber)\n    {\n        var bytes = new List<byte>\n        {\n            (byte)BlockTitle.AdaptationRead,\n            channelNumber\n        };\n\n        Log.WriteLine($\"Sending AdaptationRead block\");\n        SendBlock(bytes);\n\n        return ReceiveAdaptationBlock();\n    }\n\n    public bool AdaptationTest(byte channelNumber, ushort channelValue)\n    {\n        var bytes = new List<byte>\n        {\n            (byte)BlockTitle.AdaptationTest,\n            channelNumber,\n            (byte)(channelValue / 256),\n            (byte)(channelValue % 256)\n        };\n\n        Log.WriteLine($\"Sending AdaptationTest block\");\n        SendBlock(bytes);\n\n        return ReceiveAdaptationBlock();\n    }\n\n    public bool AdaptationSave(byte channelNumber, ushort channelValue, int workshopCode)\n    {\n        var bytes = new List<byte>\n        {\n            (byte)BlockTitle.AdaptationSave,\n            channelNumber,\n            (byte)(channelValue / 256),\n            (byte)(channelValue % 256),\n            (byte)(workshopCode >> 16),\n            (byte)((workshopCode >> 8) & 0xFF),\n            (byte)(workshopCode & 0xFF)\n        };\n\n        Log.WriteLine($\"Sending AdaptationSave block\");\n        SendBlock(bytes);\n\n        return ReceiveAdaptationBlock();\n    }\n\n    private bool ReceiveAdaptationBlock()\n    {\n        var responseBlock = ReceiveBlock();\n        if (responseBlock is NakBlock)\n        {\n            Log.WriteLine($\"Received a NAK.\");\n            return false;\n        }\n\n        if (responseBlock is not AdaptationResponseBlock adaptationResponse)\n        {\n            Log.WriteLine($\"Expected an Adaptation response block but received a ${responseBlock.Title:X2} block.\");\n            return false;\n        }\n\n        Log.WriteLine($\"Adaptation value: {adaptationResponse.ChannelValue}\");\n\n        return true;\n    }\n\n    public bool GroupRead(byte groupNumber, bool useBasicSetting = false)\n    {\n        if (groupNumber == 0)\n        {\n            return RawDataRead(useBasicSetting);\n        }\n\n        if (useBasicSetting)\n        {\n            Log.WriteLine($\"Sending Basic Setting Read blocks...\");\n        }\n        else\n        {\n            Log.WriteLine($\"Sending Group Read blocks...\");\n        }\n\n        GroupReadResponseWithTextBlock? textBlock = null;\n\n        Log.WriteLine(\"[Up arrow | Down arrow | Q to quit]\", LogDest.Console);\n        while (true)\n        {\n            if (Console.KeyAvailable)\n            {\n                var keyInfo = Console.ReadKey(intercept: true);\n                if (keyInfo.Key == ConsoleKey.UpArrow)\n                {\n                    if (groupNumber < 255)\n                    {\n                        groupNumber++;\n                    }\n                }\n                else if (keyInfo.Key == ConsoleKey.DownArrow)\n                {\n                    if (groupNumber > 1)\n                    {\n                        groupNumber--;\n                    }\n                }\n                else if (keyInfo.Key == ConsoleKey.Q)\n                {\n                    break;\n                }\n            }\n\n            var bytes = new List<byte>\n            {\n                (byte)(useBasicSetting ? BlockTitle.BasicSettingRead : BlockTitle.GroupRead),\n                groupNumber\n            };\n            SendBlock(bytes);\n\n            var responseBlock = ReceiveBlock();\n            if (responseBlock is NakBlock)\n            {\n                Overlay($\"Group {groupNumber:D3}: Not Available\");\n            }\n            else if (responseBlock is GroupReadResponseWithTextBlock groupReadResponseWithText)\n            {\n                Log.WriteLine($\"{groupReadResponseWithText}\", LogDest.File);\n                textBlock = groupReadResponseWithText;\n            }\n            else if (responseBlock is GroupReadResponseBlock groupReading)\n            {\n                Overlay($\"Group {groupNumber:D3}: {groupReading}\");\n            }\n            else if (responseBlock is RawDataReadResponseBlock rawData)\n            {\n                if (textBlock != null && rawData.Body.Count > 0)\n                {\n                    var sb = new StringBuilder($\"Group {groupNumber:D3}: \");\n                    sb.Append(textBlock.GetText(rawData.Body[0]));\n                    sb.Append(Utils.DumpDecimal(rawData.Body.Skip(1)));\n                    Overlay(sb.ToString());\n                }\n                else\n                {\n                    Overlay($\"Group {groupNumber:D3}: {rawData}\");\n                }\n            }\n            else\n            {\n                Log.WriteLine($\"Expected a Group Reading response block but received a ${responseBlock.Title:X2} block.\");\n                return false;\n            }\n        }\n        Log.WriteLine(LogDest.Console);\n\n        return true;\n    }\n\n    private bool RawDataRead(bool useBasicSetting)\n    {\n        if (useBasicSetting)\n        {\n            Log.WriteLine($\"Sending Basic Setting Raw Data Read block\");\n        }\n        else\n        {\n            Log.WriteLine($\"Sending Raw Data Read block\");\n        }\n\n        Log.WriteLine(\"[Press a key to quit]\", LogDest.Console);\n        while (!Console.KeyAvailable)\n        {\n            var bytes = new List<byte>\n            {\n                (byte)(useBasicSetting ? BlockTitle.BasicSettingRawDataRead : BlockTitle.RawDataRead)\n            };\n            SendBlock(bytes);\n\n            var responseBlock = ReceiveBlock();\n\n            if (responseBlock is not RawDataReadResponseBlock rawDataReadResponse)\n            {\n                Log.WriteLine($\"Expected a Raw Data Read response block but received a ${responseBlock.Title:X2} block.\");\n                return false;\n            }\n\n            Overlay(rawDataReadResponse.ToString());\n        }\n        Log.WriteLine(LogDest.Console);\n\n        return true;\n    }\n\n    public List<byte> ReadSecureImmoAccess(List<byte> blockBytes)\n    {\n\n        blockBytes.Insert(0, (byte)BlockTitle.SecurityImmoAccess1);\n\n        Log.WriteLine($\"Sending ReadSecureImmoAccess block: {Utils.DumpBytes(blockBytes)}\");\n\n        SendBlock(blockBytes);\n        var blocks = ReceiveBlocks();\n\n        if (blocks.Count == 1 && blocks[0] is NakBlock)\n        {\n            return [];\n        }\n\n        blocks = blocks.Where(b => !b.IsAckNak).ToList();\n        if (blocks.Count != 1)\n        {\n            throw new InvalidOperationException($\"ReadRomEeprom returned {blocks.Count} blocks instead of 1\");\n        }\n        return blocks[0].Body.ToList();\n    }\n\n    /// <summary>\n    /// Erase the current console line and replace it with message.\n    /// Also writes the message to the log.\n    /// </summary>\n    private static void Overlay(string message)\n    {\n        (int left, int top) = Console.GetCursorPosition();\n        Console.SetCursorPosition(0, top);\n        if (left > 0)\n        {\n            Log.Write(new string(' ', left), LogDest.Console);\n            Console.SetCursorPosition(0, top);\n        }\n        Log.Write(message, LogDest.Console);\n        Log.WriteLine(message, LogDest.File);\n    }\n\n    private static class TimeInterval\n    {\n        /// <summary>\n        /// Time to wait in milliseconds after receiving a byte from the ECU before sending the next byte.\n        /// Valid range: 1-50ms (according to SAE J2818)\n        /// </summary>\n        public const int R6 = 2;\n    }\n\n    public IKwpCommon KwpCommon { get; }\n\n    private bool _isConnected;\n\n    private byte? _blockCounter;\n\n    public KW1281Dialog(IKwpCommon kwpCommon)\n    {\n        KwpCommon = kwpCommon;\n        _isConnected = false;\n        _blockCounter = null;\n    }\n}\n\n/// <summary>\n/// Used for commands such as ActuatorTest which need to be kept alive with ACKs while waiting\n/// for user input.\n/// </summary>\ninternal class KW1281KeepAlive : IDisposable\n{\n    private readonly IKW1281Dialog _kw1281Dialog;\n    private volatile bool _cancel = false;\n    private Task? _keepAliveTask = null;\n\n    public KW1281KeepAlive(IKW1281Dialog kw1281Dialog)\n    {\n        _kw1281Dialog = kw1281Dialog;\n    }\n\n    public ActuatorTestResponseBlock? ActuatorTest(byte value)\n    {\n        Pause();\n        var result = _kw1281Dialog.ActuatorTest(value);\n        Resume();\n        return result;\n    }\n\n    public void Dispose()\n    {\n        Pause();\n    }\n\n    private void Pause()\n    {\n        _cancel = true;\n        if (_keepAliveTask != null)\n        {\n            _keepAliveTask.Wait();\n        }\n    }\n\n    private void Resume()\n    {\n        _keepAliveTask = Task.Run(KeepAlive);\n    }\n\n    private void KeepAlive()\n    {\n        _cancel = false;\n        while (!_cancel)\n        {\n            _kw1281Dialog.KeepAlive();\n            Log.Write(\".\", LogDest.Console);\n        }\n    }\n}\n"
  },
  {
    "path": "Kwp2000/DiagnosticService.cs",
    "content": "﻿namespace BitFab.KW1281Test.Kwp2000\n{\n    public enum DiagnosticService : byte\n    {\n        startDiagnosticSession = 0x10,\n        ecuReset = 0x11,\n        readEcuIdentification = 0x1A,\n        stopDiagnosticSession = 0x20,\n        readMemoryByAddress = 0x23,\n        securityAccess = 0x27,\n        startRoutineByLocalIdentifier = 0x31,\n        requestDownload = 0x34,\n        transferData = 0x36,\n        writeMemoryByAddress = 0x3D,\n        testerPresent = 0x3E,\n        startCommunication = 0x81,\n        stopCommunication = 0x82,\n        accessTimingParameters = 0x83,\n    };\n}\n"
  },
  {
    "path": "Kwp2000/KW2000Dialog.cs",
    "content": "﻿using BitFab.KW1281Test.Kwp2000;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading;\nusing Service = BitFab.KW1281Test.Kwp2000.DiagnosticService;\n\nnamespace BitFab.KW1281Test\n{\n    internal class KW2000Dialog\n    {\n        private const byte _testerAddress = 0xF1;\n\n        /// <summary>\n        /// Inter-command delay (milliseconds)\n        /// </summary>\n        public int P3 { get; set; } = 55;\n\n        /// <summary>\n        /// Inter-byte delay (milliseconds)\n        /// </summary>\n        public int P4 { get; set; } = 5;\n\n        public void DumpMem(uint address, uint length, string dumpFileName)\n        {\n            StartDiagnosticSession(0x84, 0x14);\n\n            Thread.Sleep(350);\n\n            Log.WriteLine($\"Saving memory dump to {dumpFileName}\");\n            DumpMemory(address, length, maxReadLength: 32, dumpFileName);\n            Log.WriteLine($\"Saved memory dump to {dumpFileName}\");\n\n            EcuReset(0x01);\n        }\n\n        private void DumpMemory(\n            uint startAddr, uint length, byte maxReadLength, string fileName)\n        {\n            using var fs = File.Create(fileName, maxReadLength, FileOptions.WriteThrough);\n            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)\n            {\n                var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);\n                try\n                {\n                    var blockBytes = ReadMemoryByAddress(addr, readLength);\n                    fs.Write(blockBytes, 0, blockBytes.Length);\n\n                    if (blockBytes.Length != readLength)\n                    {\n                        throw new InvalidOperationException(\n                            $\"Expected {readLength} bytes from ReadMemoryByAddress() but received {blockBytes.Length} bytes\");\n                    }\n                }\n                catch (NegativeResponseException)\n                {\n                    // Access not allowed?\n                    Log.WriteLine(\"Failed to read memory.\");\n                }\n                finally\n                {\n                    fs.Flush();\n                }\n            }\n        }\n\n        public void StartDiagnosticSession(byte v1, byte v2)\n        {\n            var responseMessage = SendReceive(Service.startDiagnosticSession, new[] { v1, v2 });\n            if (responseMessage.Body[0] != v1)\n            {\n                throw new InvalidOperationException($\"Unexpected diagnosticMode: {responseMessage.Body[0]:X2}\");\n            }\n        }\n\n        public void EcuReset(byte value)\n        {\n            var responseMessage = SendReceive(Service.ecuReset, new[] { value });\n        }\n\n        public byte[] ReadMemoryByAddress(uint address, byte count)\n        {\n            var addressBytes = Utils.GetBytes(address);\n\n            var responseMessage = SendReceive(Service.readMemoryByAddress,\n                new byte[]\n                {\n                    addressBytes[2], addressBytes[1], addressBytes[0],\n                    count\n                });\n\n            return responseMessage.Body.ToArray();\n        }\n\n        public byte[] WriteMemoryByAddress(uint address, byte count, byte[] data)\n        {\n            var addressBytes = Utils.GetBytes(address);\n\n            var messageBytes = new List<byte>\n            {\n                addressBytes[2],\n                addressBytes[1],\n                addressBytes[0],\n                count\n            };\n            messageBytes.AddRange(data);\n\n            var responseMessage = SendReceive(Service.writeMemoryByAddress,\n                messageBytes.ToArray());\n\n            return responseMessage.Body.ToArray();\n        }\n\n        public Kwp2000Message SendReceive(\n            Service service, byte[] body, bool excludeAddresses = false)\n        {\n            SendMessage(service, body, excludeAddresses);\n\n            while (true)\n            {\n                var message = ReceiveMessage();\n\n                if (message.SrcAddress.HasValue)\n                {\n                    if (message.SrcAddress != _controllerAddress)\n                    {\n                        throw new InvalidOperationException($\"Unexpected SrcAddress: {message.SrcAddress:X2}\");\n                    }\n\n                    if (message.DestAddress != _testerAddress)\n                    {\n                        throw new InvalidOperationException($\"Unexpected DestAddress: {message.DestAddress:X2}\");\n                    }\n                }\n\n                if ((byte)message.Service == 0x7F)\n                {\n                    if (message.Body[0] == (byte)service &&\n                        message.Body[1] == (byte)ResponseCode.reqCorrectlyRcvdRspPending)\n                    {\n                        continue;\n                    }\n                    throw new NegativeResponseException(message);\n                }\n\n                if (!message.IsPositiveResponse(service))\n                {\n                    throw new InvalidOperationException($\"Unexpected response: {message.Service}\");\n                }\n\n                return message;\n            }\n        }\n\n        public void SendMessage(Service service, byte[] body, bool excludeAddresses = false)\n        {\n            static void Sleep(int ms)\n            {\n                var maxTick = Stopwatch.GetTimestamp() + Stopwatch.Frequency / 1000 * ms;\n                while (Stopwatch.GetTimestamp() < maxTick)\n                    ;\n            }\n\n            Kwp2000Message message;\n            if (excludeAddresses)\n            {\n                message = new Kwp2000Message(service, body);\n            }\n            else\n            {\n                message = new Kwp2000Message(\n                    _controllerAddress, _testerAddress, service, body);\n            }\n            var checksum = message.CalcChecksum();\n            Sleep(P3);\n\n            foreach (var b in message.HeaderBytes)\n            {\n                _kwpCommon.WriteByte(b);\n                Sleep(P4);\n            }\n\n            _kwpCommon.WriteByte((byte)message.Service);\n            Sleep(P4);\n\n            foreach (var b in message.Body)\n            {\n                _kwpCommon.WriteByte(b);\n                Sleep(P4);\n            }\n\n            _kwpCommon.WriteByte(checksum);\n\n            Log.WriteLine($\"Sent: {message}\");\n        }\n\n        public Kwp2000Message ReceiveMessage()\n        {\n            var formatByte = _kwpCommon.ReadByte();\n            byte? destAddress = null;\n            byte? srcAddress = null;\n            if ((formatByte & 0x80) == 0x80)\n            {\n                destAddress = _kwpCommon.ReadByte();\n                srcAddress = _kwpCommon.ReadByte();\n            }\n            byte? lengthByte = null;\n            if ((formatByte & 63) == 0)\n            {\n                lengthByte = _kwpCommon.ReadByte();\n            }\n            var bodyLength = (lengthByte ?? (formatByte & 63)) - 1;\n            var service = (Service)_kwpCommon.ReadByte();\n            var body = new List<byte>();\n            for (var i = 0; i < bodyLength; i++)\n            {\n                body.Add(_kwpCommon.ReadByte());\n            }\n            var checksum = _kwpCommon.ReadByte();\n\n            var message = new Kwp2000Message(\n                formatByte, destAddress, srcAddress, lengthByte, service, body, checksum);\n            Log.WriteLine($\"Received: {message}\");\n            return message;\n        }\n\n        private readonly IKwpCommon _kwpCommon;\n        private readonly byte _controllerAddress;\n\n        public KW2000Dialog(IKwpCommon kwpCommon, byte controllerAddress)\n        {\n            _kwpCommon = kwpCommon;\n            _controllerAddress = controllerAddress;\n        }\n    }\n}\n"
  },
  {
    "path": "Kwp2000/Kwp2000Message.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Text;\n\nnamespace BitFab.KW1281Test.Kwp2000\n{\n    public class Kwp2000Message\n    {\n        public byte FormatByte { get; }\n\n        public byte? DestAddress { get; }\n\n        public byte? SrcAddress { get; }\n\n        public byte? LengthByte { get; }\n\n        public DiagnosticService Service { get; }\n\n        public List<byte> Body { get; }\n\n        public byte CalcChecksum()\n        {\n            return (byte)(\n                FormatByte +\n                (DestAddress ?? 0) +\n                (SrcAddress ?? 0) +\n                (LengthByte ?? 0) +\n                (byte)Service +\n                Body.Sum(b => b));\n        }\n\n        public Kwp2000Message(\n            DiagnosticService service, IList<byte> body)\n        {\n            FormatByte = CalcFormatByte(body, excludeAddresses: true);\n            DestAddress = null;\n            SrcAddress = null;\n            LengthByte = CalcLengthByte(body);\n            Service = service;\n            Body = new List<byte>(body);\n        }\n\n        public Kwp2000Message(\n            byte destAddress, byte srcAddress, DiagnosticService service, IList<byte> body)\n        {\n            FormatByte = CalcFormatByte(body);\n            DestAddress = destAddress;\n            SrcAddress = srcAddress;\n            LengthByte = CalcLengthByte(body);\n            Service = service;\n            Body = new List<byte>(body);\n        }\n\n        public Kwp2000Message(\n            byte formatByte, byte? destAddress, byte? srcAddress, byte? lengthByte,\n            DiagnosticService service,\n            IList<byte> body, byte checksum)\n        {\n            FormatByte = formatByte;\n            DestAddress = destAddress;\n            SrcAddress = srcAddress;\n            LengthByte = lengthByte;\n            Service = service;\n            Body = new List<byte>(body);\n\n            Debug.Assert(FormatByte == CalcFormatByte(Body, !destAddress.HasValue));\n            Debug.Assert(LengthByte == CalcLengthByte(Body));\n            Debug.Assert(checksum == CalcChecksum());\n        }\n\n        public IEnumerable<byte> HeaderBytes\n        {\n            get\n            {\n                yield return FormatByte;\n                if (DestAddress.HasValue)\n                {\n                    yield return DestAddress.Value;\n                }\n                if (SrcAddress.HasValue)\n                {\n                    yield return SrcAddress.Value;\n                }\n                if (LengthByte.HasValue)\n                {\n                    yield return LengthByte.Value;\n                }\n            }\n        }\n\n        public override string ToString()\n        {\n            var sb = new StringBuilder();\n            foreach(var b in HeaderBytes)\n            {\n                sb.Append($\"{b:X2} \");\n            }\n            sb.Append($\"{(byte)Service:X2}\");\n            sb.Append(Utils.Dump(Body));\n            sb.Append($\" {CalcChecksum():X2}\");\n            sb.Append($\" ({DescribeService()})\");\n            return sb.ToString();\n        }\n\n        public bool IsPositiveResponse(DiagnosticService service)\n        {\n            return ((byte)service | 0x40) == (byte)Service;\n        }\n\n        public string DescribeService()\n        {\n            var serviceByte = (byte)Service;\n            if (serviceByte == 0x7F)\n            {\n                return $\"{(DiagnosticService)Body[0]} NAK {(ResponseCode)Body[1]}\";\n            }\n\n            var bareServiceByte = (byte)(serviceByte & ~0x40);\n\n            string bareServiceString;\n            if (Enum.TryParse(bareServiceByte.ToString(), out DiagnosticService bareService))\n            {\n                bareServiceString = bareService.ToString();\n            }\n            else\n            {\n                bareServiceString = $\"{bareServiceByte:X2}\";\n            }\n\n            if ((serviceByte & 0x40) == 0x40)\n            {\n                return $\"{bareServiceString} ACK\";\n            }\n            else\n            {\n                return bareServiceString;\n            }\n        }\n\n        private static byte CalcFormatByte(IList<byte> body, bool excludeAddresses = false)\n        {\n            var length = body.Count + 1;\n            byte formatByte = (byte)(length > 63 ? 0 : length);\n            if (!excludeAddresses)\n            {\n                formatByte |= 0x80;\n            }\n            return formatByte;\n        }\n\n        private static byte? CalcLengthByte(IList<byte> body)\n        {\n            var length = body.Count + 1;\n            return length > 63 ? (byte)length : null;\n        }\n    }\n}\n"
  },
  {
    "path": "Kwp2000/NegativeResponseException.cs",
    "content": "﻿using System;\n\nnamespace BitFab.KW1281Test.Kwp2000\n{\n    public class NegativeResponseException : Exception\n    {\n        public NegativeResponseException(Kwp2000Message kwp2000Message)\n        {\n            Kwp2000Message = kwp2000Message;\n        }\n\n        public Kwp2000Message Kwp2000Message { get; }\n    }\n}\n"
  },
  {
    "path": "Kwp2000/ResponseCode.cs",
    "content": "﻿namespace BitFab.KW1281Test.Kwp2000\n{\n    /// <summary>\n    /// \n    /// </summary>\n    public enum ResponseCode\n    {\n        generalReject = 0x10,\n        serviceNotSupported = 0x11,\n        subFunctionNotSupportedInvalidFormat = 0x12,\n        busyRepeatRequest = 0x21,\n        conditionsNotCorrectOrRequestSequenceError = 0x22,\n        routineNotComplete = 0x23,\n        requestOutOfRange = 0x31,\n        securityAccessDenied = 0x33,\n        invalidKey = 0x35,\n        exceedNumberOfAttempts = 0x36,\n        requiredTimeDelayNotExpired = 0x37,\n        downloadNotAccepted = 0x40,\n        improperDownloadType = 0x41,\n        cantDownloadToSpecifiedAddress = 0x42,\n        cantDownloadNumberOfBytesRequested = 0x43,\n        uploadNotAccepted = 0x50,\n        improperUploadType = 0x51,\n        cantUploadFromSpecifiedAddress = 0x52,\n        cantUploadNumberOfBytesRequested = 0x53,\n        transferSuspended = 0x71,\n        transferAborted = 0x72,\n        illegalAddressInBlockTransfer = 0x74,\n        illegalByteCountInBlockTransfer = 0x75,\n        illegalBlockTransferType = 0x76,\n        blockTransferDataChecksumError = 0x77,\n        reqCorrectlyRcvdRspPending = 0x78,\n        incorrectByteCountDuringBlockTransfer = 0x79,\n        // Manufacturer-Specific 0x80-0xFF\n    }\n}\n"
  },
  {
    "path": "KwpCommon.cs",
    "content": "﻿using BitFab.KW1281Test.Interface;\nusing System;\nusing System.Collections.Generic;\nusing System.Runtime;\nusing System.Threading;\n\nnamespace BitFab.KW1281Test\n{\n    public interface IKwpCommon\n    {\n        IInterface Interface { get; }\n\n        int WakeUp(byte controllerAddress, bool evenParity = false, bool failQuietly = false);\n\n        byte ReadByte();\n\n        /// <summary>\n        /// Write a byte to the interface and receive its echo.\n        /// </summary>\n        /// <param name=\"b\">The byte to write.</param>\n        void WriteByte(byte b);\n\n        void ReadComplement(byte b);\n    }\n\n    internal class KwpCommon : IKwpCommon\n    {\n        public IInterface Interface { get; }\n\n        public int WakeUp(byte controllerAddress, bool evenParity, bool failQuietly)\n        {\n            // Disable garbage collection int this time-critical method\n            var noGC = GC.TryStartNoGCRegion(1024 * 1024);\n            if (!noGC)\n            {\n                Log.WriteLine(\"Warning: Unable to disable GC so timing may be compromised.\");\n            }\n\n            var protocolVersion = 0;\n            Interface.ReadTimeout = (int)TimeSpan.FromSeconds(2).TotalMilliseconds;\n            try\n            {\n                const int maxTries = 3;\n                for (var i = 1; i <= maxTries; i++)\n                {\n                    try\n                    {\n                        protocolVersion = WakeUpNoRetry(controllerAddress, evenParity);\n                        break;\n                    }\n                    catch (Exception ex)\n                    {\n                        Log.WriteLine(ex.Message);\n\n                        if (i < maxTries)\n                        {\n                            Log.WriteLine(\"Retrying wakeup message...\");\n                            Thread.Sleep(TimeSpan.FromSeconds(1));\n                        }\n                        else\n                        {\n                            if (!failQuietly)\n                            {\n                                Log.WriteLine();\n                                Log.WriteLine(\"Controller did not wake up.\");\n                                Log.WriteLine(\"    - Are you using a supported cable?\");\n                                Log.WriteLine(\"    - Is the cable plugged in and any necessary drivers installed?\");\n                                Log.WriteLine(\"    - Is the ignition on?\");\n                                Log.WriteLine(\"    - Is the controller address correct?\");\n                                Log.WriteLine(\"    - Is the baud rate correct (unexpected sync byte errors)? Try 10400, 9600, 4800.\");\n                                Log.WriteLine(\"You can try other software (e.g. VCDS-Lite) to verify that the cable/drivers/address are ok.\");\n                            }\n                            throw new UnableToProceedException();\n                        }\n                    }\n                }\n            }\n            finally\n            {\n                if (GCSettings.LatencyMode == GCLatencyMode.NoGCRegion)\n                {\n                    GC.EndNoGCRegion();\n                }\n                Interface.ReadTimeout = Interface.DefaultTimeoutMilliseconds;\n            }\n\n            return protocolVersion;\n        }\n\n        private int WakeUpNoRetry(byte controllerAddress, bool evenParity)\n        {\n            Thread.Sleep(300);\n\n            BitBang5Baud(controllerAddress, evenParity);\n\n            // Throw away anything that might be in the receive buffer\n            Interface.ClearReceiveBuffer();\n\n            Log.WriteLine(\"Reading sync byte\");\n\n            // Buffer logging in memory until we're done with the wakeup, which is sensitive to timing\n            var logLines = new List<string>();\n\n            var syncByte = Interface.ReadByte();\n\n            if (syncByte != 0x55)\n            {\n                throw new InvalidOperationException(\n                    $\"Unexpected sync byte: Expected $55, Actual ${syncByte:X2}\");\n            }\n\n            int protocolVersion;\n            try\n            {\n                var keywordLsb = Interface.ReadByte();\n                logLines.Add($\"Keyword Lsb ${keywordLsb:X2}\");\n\n                var keywordMsb = ReadByte();\n                logLines.Add($\"Keyword Msb ${keywordMsb:X2}\");\n\n                protocolVersion = ((keywordMsb & 0x7F) << 7) + (keywordLsb & 0x7F);\n                logLines.Add($\"Protocol is KW {protocolVersion} (8N1)\");\n\n                BusyWait.Delay(25);\n\n                var complement = (byte)~keywordMsb;\n                WriteByte(complement);\n            }\n            finally\n            {\n                foreach (var line in logLines)\n                {\n                    Log.WriteLine(line);\n                }\n            }\n\n            if (protocolVersion >= 2000)\n            {\n                ReadComplement(\n                    Utils.AdjustParity(controllerAddress, evenParity));\n            }\n\n            return protocolVersion;\n        }\n\n        public byte ReadByte()\n        {\n            return Interface.ReadByte();\n        }\n\n        public void WriteByte(byte b)\n        {\n            WriteByteAndDiscardEcho(b);\n        }\n\n        public void ReadComplement(byte b)\n        {\n            var expectedComplement = (byte)~b;\n            var actualComplement = Interface.ReadByte();\n            if (actualComplement != expectedComplement)\n            {\n                throw new InvalidOperationException(\n                    $\"Received complement ${actualComplement:X2} but expected ${expectedComplement:X2}\");\n            }\n        }\n\n        /// <summary>\n        /// Send a byte at 5 baud manually to the interface. The byte will be sent as\n        /// 1 start bit, 7 data bits, 1 parity bit (even or odd), 1 stop bit.\n        /// https://www.blafusel.de/obd/obd2_kw1281.html\n        /// </summary>\n        /// <param name=\"b\">The byte to send.</param>\n        /// <param name=\"evenParity\">\n        /// False for odd parity (KWP1281), true for even parity (KWP2000).</param>\n        private void BitBang5Baud(byte b, bool evenParity)\n        {\n            b = Utils.AdjustParity(b, evenParity);\n\n            const int bitsPerSec = 5;\n            const long msPerBit = 1000 / bitsPerSec;\n\n            var waiter = new BusyWait(msPerBit);\n\n            // The first call to SetBreak takes extra time (at least with an FTDI cable on Linux)\n            // so do that here outside of the timing loop. Since the break state should already be\n            // false, this should have no effect other than to delay a couple milliseconds and it\n            // makes the timing of the rest of the bits be more accurate.\n            Interface.SetBreak(false);\n\n            BitBang(false); // Start bit\n\n            for (int i = 0; i < 8; i++)\n            {\n                bool bit = (b & 1) == 1;\n                BitBang(bit);\n                b >>= 1;\n            }\n\n            BitBang(true); // Stop bit\n\n            BusyWait.Delay(msPerBit);\n            return;\n\n            // Delay the appropriate amount and then set/clear the TxD line\n            void BitBang(bool bit)\n            {\n                waiter.DelayUntilNextCycle();\n                Interface.SetBreak(!bit);\n            }\n        }\n\n        /// <summary>\n        /// Write a byte to the interface and read/discard its echo.\n        /// </summary>\n        private void WriteByteAndDiscardEcho(byte b)\n        {\n            Interface.WriteByteRaw(b);\n            var echo = Interface.ReadByte();\n#if false\n            if (echo != b)\n            {\n                throw new InvalidOperationException($\"Wrote 0x{b:X2} to port but echo was 0x{echo:X2}\");\n            }\n#endif\n        }\n\n        public KwpCommon(IInterface @interface)\n        {\n            Interface = @interface;\n        }\n    }\n}\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "﻿MIT License\n\nCopyright © 2021 Greg Menounos\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Logging/ConsoleLog.cs",
    "content": "﻿using System;\n\nnamespace BitFab.KW1281Test.Logging\n{\n    internal class ConsoleLog : ILog\n    {\n        public void Write(string message, LogDest dest)\n        {\n            if (dest != LogDest.File)\n            {\n                Console.Write(message);\n            }\n        }\n\n        public void WriteLine(LogDest dest)\n        {\n            if (dest != LogDest.File)\n            {\n                Console.WriteLine();\n            }\n        }\n\n        public void WriteLine(string message, LogDest dest)\n        {\n            if (dest != LogDest.File)\n            {\n                Console.WriteLine(message);\n            }\n        }\n\n        public void Close()\n        {\n        }\n\n        public void Dispose()\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "Logging/FileLog.cs",
    "content": "﻿using System;\nusing System.IO;\n\nnamespace BitFab.KW1281Test.Logging\n{\n    internal class FileLog : ILog\n    {\n        private readonly StreamWriter _writer;\n\n        public FileLog(string filename)\n        {\n            _writer = new StreamWriter(filename, append: true);\n        }\n\n        public void WriteLine(string message, LogDest dest)\n        {\n            if (dest != LogDest.File)\n            {\n                Console.WriteLine(message);\n            }\n            if (dest != LogDest.Console)\n            {\n                _writer.WriteLine(message);\n            }\n        }\n\n        public void WriteLine(LogDest dest)\n        {\n            if (dest != LogDest.File)\n            {\n                Console.WriteLine();\n            }\n            if (dest != LogDest.Console)\n            {\n                _writer.WriteLine();\n            }\n        }\n\n        public void Write(string message, LogDest dest)\n        {\n            if (dest != LogDest.File)\n            {\n                Console.Write(message);\n            }\n            if (dest != LogDest.Console)\n            {\n                _writer.Write(message);\n            }\n        }\n\n        public void Close()\n        {\n            _writer.Close();\n        }\n\n        public void Dispose()\n        {\n            Close();\n        }\n    }\n}\n"
  },
  {
    "path": "Logging/ILog.cs",
    "content": "﻿using System;\n\nnamespace BitFab.KW1281Test.Logging\n{\n    internal enum LogDest\n    {\n        All,\n        Console,\n        File\n    }\n\n    internal interface ILog : IDisposable\n    {\n        void Write(string message, LogDest dest = LogDest.All);\n\n        void WriteLine(LogDest dest = LogDest.All);\n\n        void WriteLine(string message, LogDest dest = LogDest.All);\n\n        void Close();\n    }\n}\n"
  },
  {
    "path": "Program.cs",
    "content": "﻿global using static BitFab.KW1281Test.Program;\n\nusing BitFab.KW1281Test.Interface;\nusing BitFab.KW1281Test.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.Linq;\nusing System.Reflection;\nusing System.Runtime.CompilerServices;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing BitFab.KW1281Test.EDC15;\nusing System.Runtime.InteropServices;\nusing System.IO;\n\n[assembly: InternalsVisibleTo(\"BitFab.KW1281Test.Tests\")]\n\nnamespace BitFab.KW1281Test;\n\nclass Program\n{\n    public static ILog Log { get; private set; } = new ConsoleLog();\n\n    internal static List<string> CommandAndArgs { get; private set; } = [];\n\n    static void Main(string[] args)\n    {\n        try\n        {\n            Log = new FileLog(\"KW1281Test.log\");\n\n            CommandAndArgs.Add(\n                Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0]));\n            CommandAndArgs.AddRange(args);\n\n            var tester = new Program();\n            tester.Run(args);\n        }\n        catch (UnableToProceedException)\n        {\n        }\n        catch (Exception ex)\n        {\n            Log.WriteLine($\"Caught: {ex.GetType()} {ex.Message}\");\n            Log.WriteLine($\"Unhandled exception: {ex}\");\n        }\n        finally\n        {\n            Log.Close();\n        }\n    }\n\n    void Run(string[] args)\n    {\n        Console.ForegroundColor = ConsoleColor.Green;\n        Console.Write(\"KW1281Test: Yesterday's diagnostics...\");\n        Thread.Sleep(2000);\n        Console.WriteLine(\"Today.\");\n        Thread.Sleep(2000);\n        Console.ResetColor();\n        Console.WriteLine();\n\n        var version = GetType().GetTypeInfo().Assembly\n            .GetCustomAttribute<AssemblyInformationalVersionAttribute>()!\n            .InformationalVersion;\n        Log.WriteLine($\"Version {version} (https://github.com/gmenounos/kw1281test/releases)\");\n        Log.WriteLine($\"Command Line: {string.Join(' ', CommandAndArgs)}\");\n        Log.WriteLine($\"OSVersion: {Environment.OSVersion}\");\n        Log.WriteLine($\".NET Version: {Environment.Version}\");\n        Log.WriteLine($\"Culture: {CultureInfo.InstalledUICulture}\");\n\n        if (args.Length < 4)\n        {\n            ShowUsage();\n            return;\n        }\n\n        try\n        {\n            // This seems to increase the accuracy of our timing loops\n            Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;\n        }\n        catch(Win32Exception)\n        {\n            // Ignore if we don't have permission to increase our priority\n        }\n\n        string portName = args[0];\n        var baudRate = int.Parse(args[1]);\n        int controllerAddress = int.Parse(args[2], NumberStyles.HexNumber);\n        var command = args[3];\n        uint address = 0;\n        uint length = 0;\n        byte value = 0;\n        int softwareCoding = 0;\n        int workshopCode = 0;\n        byte channel = 0;\n        ushort channelValue = 0;\n        ushort? login = null;\n        byte groupNumber = 0;\n        var addressValuePairs = new List<KeyValuePair<ushort, byte>>();\n\n        if (string.Compare(command, \"ReadEeprom\", ignoreCase: true) == 0 ||\n            string.Compare(command, \"ReadRAM\", ignoreCase: true) == 0 ||\n            string.Compare(command, \"ReadROM\", ignoreCase: true) == 0 ||\n            string.Compare(command, \"WriteRAM\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 5)\n            {\n                ShowUsage();\n                return;\n            }\n\n            address = Utils.ParseUint(args[4]);\n        }\n        else if (string.Compare(command, \"DumpMarelliMem\", ignoreCase: true) == 0 ||\n                 string.Compare(command, \"DumpEeprom\", ignoreCase: true) == 0 ||\n                 string.Compare(command, \"DumpMem\", ignoreCase: true) == 0 ||\n                 string.Compare(command, \"DumpRam\", ignoreCase: true) == 0 ||\n                 string.Compare(command, \"DumpRBxMem\", ignoreCase: true) == 0 ||\n                 string.Compare(command, \"DumpRBxMemOdd\", ignoreCase: true) == 0 ||\n                 string.Compare(command, \"DumpRom\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 6)\n            {\n                ShowUsage();\n                return;\n            }\n\n            address = Utils.ParseUint(args[4]);\n            length = Utils.ParseUint(args[5]);\n\n            if (args.Length > 6)\n            {\n                _filename = args[6];\n            }\n        }\n        else if (string.Compare(command, \"WriteEeprom\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 6)\n            {\n                ShowUsage();\n                return;\n            }\n\n            address = Utils.ParseUint(args[4]);\n            value = (byte)Utils.ParseUint(args[5]);\n        }\n        else if (string.Compare(command, \"LoadEeprom\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 6)\n            {\n                ShowUsage();\n                return;\n            }\n\n            address = Utils.ParseUint(args[4]);\n            _filename = args[5];\n        }\n        else if (string.Compare(command, \"SetSoftwareCoding\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 6)\n            {\n                ShowUsage();\n                return;\n            }\n\n            softwareCoding = (int)Utils.ParseUint(args[4]);\n            if (softwareCoding > 32767)\n            {\n                Log.WriteLine(\"SoftwareCoding cannot be greater than 32767.\");\n                return;\n            }\n            workshopCode = (int)Utils.ParseUint(args[5]);\n            if (workshopCode > 99999)\n            {\n                Log.WriteLine(\"WorkshopCode cannot be greater than 99999.\");\n                return;\n            }\n        }\n        else if (string.Compare(command, \"DumpEdc15Eeprom\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 4)\n            {\n                ShowUsage();\n                return;\n            }\n\n            if (args.Length > 4)\n            {\n                _filename = args[4];\n            }\n        }\n        else if (string.Compare(command, \"WriteEdc15Eeprom\", ignoreCase: true) == 0)\n        {\n            // WriteEdc15Eeprom ADDRESS1 VALUE1 [ADDRESS2 VALUE2 ... ADDRESSn VALUEn]\n\n            if (args.Length < 6)\n            {\n                ShowUsage();\n                return;\n            }\n\n            var dateString = DateTime.Now.ToString(\"s\").Replace(':', '-');\n            _filename = $\"EDC15_EEPROM_{dateString}.bin\";\n            \n            if (!ParseAddressesAndValues(args.Skip(4).ToList(), out addressValuePairs))\n            {\n                ShowUsage();\n                return;\n            }\n        }\n        else if (string.Compare(command, \"AdaptationRead\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 5)\n            {\n                ShowUsage();\n                return;\n            }\n\n            channel = byte.Parse(args[4]);\n\n            if (args.Length > 5)\n            {\n                login = ushort.Parse(args[5]);\n            }\n        }\n        else if (\n            string.Compare(command, \"AdaptationSave\", ignoreCase: true) == 0 ||\n            string.Compare(command, \"AdaptationTest\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 6)\n            {\n                ShowUsage();\n                return;\n            }\n\n            channel = byte.Parse(args[4]);\n            channelValue = ushort.Parse(args[5]);\n\n            if (args.Length > 6)\n            {\n                login = ushort.Parse(args[6]);\n            }\n        }\n        else if (\n            string.Compare(command, \"BasicSetting\", ignoreCase: true) == 0 ||\n            string.Compare(command, \"GroupRead\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 5)\n            {\n                ShowUsage();\n                return;\n            }\n\n            groupNumber = byte.Parse(args[4]);\n        }\n        else if (\n            string.Compare(command, \"FindLogins\", ignoreCase: true) == 0)\n        {\n            if (args.Length < 5)\n            {\n                ShowUsage();\n                return;\n            }\n\n            login = ushort.Parse(args[4]);\n        }\n\n        using var @interface = OpenPort(portName, baudRate);\n        var tester = new Tester(@interface, controllerAddress);\n        \n        switch (command.ToLower())\n        {\n            case \"autoscan\":\n                AutoScan(@interface);\n                return;\n\n            case \"dumprbxmem\":\n                tester.DumpRBxMem(address, length, _filename);\n                tester.EndCommunication();\n                return;\n\n            case \"dumprbxmemodd\":\n                tester.DumpRBxMem(address, length, _filename, evenParityWakeup: false);\n                tester.EndCommunication();\n                return;\n\n            case \"getskc\":\n                tester.GetSkc();\n                tester.EndCommunication();\n                return;\n\n            case \"togglerb4mode\":\n                tester.ToggleRB4Mode();\n                tester.EndCommunication();\n                return;\n\n            default:\n                break;\n        }\n\n        ControllerInfo ecuInfo = tester.Kwp1281Wakeup();\n\n        switch (command.ToLower())\n        {\n            case \"actuatortest\":\n                tester.ActuatorTest();\n                break;\n\n            case \"adaptationread\":\n                tester.AdaptationRead(channel, login, ecuInfo.WorkshopCode);\n                break;\n\n            case \"adaptationsave\":\n                tester.AdaptationSave(channel, channelValue, login, ecuInfo.WorkshopCode);\n                break;\n\n            case \"adaptationtest\":\n                tester.AdaptationTest(channel, channelValue, login, ecuInfo.WorkshopCode);\n                break;\n\n            case \"basicsetting\":\n                tester.BasicSettingRead(groupNumber);\n                break;\n\n            case \"clarionvwpremium4safecode\":\n                tester.ClarionVWPremium4SafeCode();\n                break;\n\n            case \"clearfaultcodes\":\n                tester.ClearFaultCodes();\n                break;\n\n            case \"delcovwpremium5safecode\":\n                tester.DelcoVWPremium5SafeCode();\n                break;\n\n            case \"dumpccmrom\":\n                tester.DumpCcmRom(_filename);\n                break;\n\n            case \"dumpclusternecrom\":\n                tester.DumpClusterNecRom(_filename);\n                break;\n\n            case \"dumpedc15eeprom\":\n            {\n                var eeprom = tester.ReadWriteEdc15Eeprom(_filename);\n                Edc15VM.DisplayEepromInfo(eeprom);\n            }\n                break;\n\n            case \"dumpeeprom\":\n                tester.DumpEeprom(address, length, _filename);\n                break;\n\n            case \"dumpmarellimem\":\n                tester.DumpMarelliMem(address, length, ecuInfo, _filename);\n                return;\n\n            case \"dumpmem\":\n                tester.DumpMem(address, length, _filename);\n                break;\n\n            case \"dumpram\":\n                tester.DumpRam(address, length, _filename);\n                break;\n\n            case \"dumprom\":\n                tester.DumpRom(address, length, _filename);\n                break;\n\n            case \"findlogins\":\n                tester.FindLogins(login!.Value, ecuInfo.WorkshopCode);\n                break;\n\n            case \"getclusterid\":\n                tester.GetClusterId();\n                break;\n\n            case \"groupread\":\n                tester.GroupRead(groupNumber);\n                break;\n\n            case \"loadeeprom\":\n                tester.LoadEeprom(address, _filename!);\n                break;\n\n            case \"mapeeprom\":\n                tester.MapEeprom(_filename);\n                break;\n\n            case \"readeeprom\":\n                tester.ReadEeprom(address);\n                break;\n\n            case \"readram\":\n                tester.ReadRam(address);\n                break;\n\n            case \"readrom\":\n                tester.ReadRom(address);\n                break;\n\n            case \"readfaultcodes\":\n                tester.ReadFaultCodes();\n                break;\n\n            case \"readident\":\n                tester.ReadIdent();\n                break;\n\n            case \"readsoftwareversion\":\n                tester.ReadSoftwareVersion();\n                break;\n\n            case \"reset\":\n                tester.Reset();\n                break;\n\n            case \"setsoftwarecoding\":\n                tester.SetSoftwareCoding(softwareCoding, workshopCode);\n                break;\n\n            case \"writeedc15eeprom\":\n                tester.ReadWriteEdc15Eeprom(_filename, addressValuePairs);\n                break;\n\n            case \"writeeeprom\":\n                tester.WriteEeprom(address, value);\n                break;\n\n            case \"writeram\":\n                tester.WriteRam(address, value);\n                break;\n\n            default:\n                ShowUsage();\n                break;\n        }\n\n        tester.EndCommunication();\n    }\n\n    private static void AutoScan(IInterface @interface)\n    {\n        var kwp1281Addresses = new List<string>();\n        var kwp2000Addresses = new List<string>();\n        foreach (var evenParity in new bool[] { false, true })\n        {\n            var parity = evenParity ? \"(EvenParity)\" : \"\";\n            for (var address = 0; address < 0x80; address++)\n            {\n                var tester = new Tester(@interface, address);\n                try\n                {\n                    Log.WriteLine($\"Attempting to wake up controller at address {address:X}{parity}...\");\n                    tester.Kwp1281Wakeup(evenParity, failQuietly: true);\n                    tester.EndCommunication();\n                    kwp1281Addresses.Add($\"{address:X}{parity}\");\n                }\n                catch (UnableToProceedException)\n                {\n                }\n                catch (UnexpectedProtocolException)\n                {\n                    kwp2000Addresses.Add($\"{address:X}{parity}\");\n                }\n            }\n        }\n\n        Log.WriteLine($\"AutoScan Results:\");\n        Log.WriteLine($\"KWP1281: {string.Join(' ', kwp1281Addresses)}\");\n        Log.WriteLine($\"KWP2000: {string.Join(' ', kwp2000Addresses)}\");\n    }\n\n    /// <summary>\n    /// Accept a series of string values in the format:\n    /// ADDRESS1 VALUE1 [ADDRESS2 VALUE2 ... ADDRESSn VALUEn]\n    ///     ADDRESS = EEPROM address in decimal (0-511) or hex ($00-$1FF)\n    ///     VALUE = Value to be stored at address in decimal (0-255) or hex ($00-$FF)\n    /// </summary>\n    internal static bool ParseAddressesAndValues(\n        List<string> addressesAndValues,\n        out List<KeyValuePair<ushort, byte>> addressValuePairs)\n    {\n        addressValuePairs = [];\n\n        if (addressesAndValues.Count % 2 != 0)\n        {\n            return false;\n        }\n\n        for (var i = 0; i < addressesAndValues.Count; i += 2)\n        {\n            uint address;\n            var valueToParse = addressesAndValues[i];\n            try\n            {\n                address = Utils.ParseUint(valueToParse);\n            }\n            catch (Exception)\n            {\n                Log.WriteLine($\"Invalid address (bad format): {valueToParse}.\");\n                return false;\n            }\n\n            if (address > 0x1FF)\n            {\n                Log.WriteLine($\"Invalid address (too large): {valueToParse}.\");\n                return false;\n            }\n\n            uint value;\n            valueToParse = addressesAndValues[i + 1];\n            try\n            {\n                value = Utils.ParseUint(valueToParse);\n            }\n            catch (Exception)\n            {\n                Log.WriteLine($\"Invalid value (bad format): {valueToParse}.\");\n                return false;\n            }\n\n            if (value > 0xFF)\n            {\n                Log.WriteLine($\"Invalid value (too large): {valueToParse}.\");\n                return false;\n            }\n\n            addressValuePairs.Add(new KeyValuePair<ushort, byte>((ushort)address, (byte)value));\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// Opens the serial port.\n    /// </summary>\n    /// <param name=\"portName\">\n    /// Either the device name of a serial port (e.g. COM1, /dev/tty23)\n    /// or an FTDI USB->Serial device serial number (2 letters followed by 6 letters/numbers).\n    /// </param>\n    /// <param name=\"baudRate\"></param>\n    /// <returns></returns>\n    private static IInterface OpenPort(string portName, int baudRate)\n    {\n        if (Regex.IsMatch(portName.ToUpper(), @\"\\A[A-Z0-9]{8}\\Z\"))\n        {\n            Log.WriteLine($\"Opening FTDI serial port {portName}\");\n            return new FtdiInterface(portName, baudRate);\n        }\n        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&\n            portName.StartsWith(\"/dev/\", StringComparison.CurrentCultureIgnoreCase))\n        {\n            Log.WriteLine($\"Opening Linux serial port {portName}\");\n            return new LinuxInterface(portName, baudRate);\n        }\n        else\n        {\n            Log.WriteLine($\"Opening Generic serial port {portName}\");\n            return new GenericInterface(portName, baudRate);\n        }\n    }\n\n    private static void ShowUsage()\n    {\n        Log.WriteLine(\"\"\"\nUsage: KW1281Test PORT BAUD ADDRESS COMMAND [args]\n                \nPORT = COM1|COM2|etc. (Windows)\n    /dev/ttyXXXX (Linux)\n    AABBCCDD (macOS/Linux FTDI cable serial number)\nBAUD = 10400|9600|etc.\nADDRESS = Controller address, e.g. 1 (ECU), 17 (cluster), 46 (CCM), 56 (radio)\nCOMMAND =\n    ActuatorTest\n    AdaptationRead CHANNEL [LOGIN]\n        CHANNEL = Channel number (0-99)\n        LOGIN = Optional login (0-65535)\n    AdaptationSave CHANNEL VALUE [LOGIN]\n        CHANNEL = Channel number (0-99)\n        VALUE = Channel value (0-65535)\n        LOGIN = Optional login (0-65535)\n    AdaptationTest CHANNEL VALUE [LOGIN]\n        CHANNEL = Channel number (0-99)\n        VALUE = Channel value (0-65535)\n        LOGIN = Optional login (0-65535)\n    AutoScan\n    BasicSetting GROUP\n        GROUP = Group number (0-255)\n        (Group 0: Raw controller data)\n    ClarionVWPremium4SafeCode\n    ClearFaultCodes\n    DelcoVWPremium5SafeCode\n    DumpEdc15Eeprom [FILENAME]\n        FILENAME = Optional filename\n    DumpEeprom START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 0) or hex (e.g. 0x0)\n        LENGTH = Number of bytes in decimal (e.g. 2048) or hex (e.g. 0x800)\n        FILENAME = Optional filename\n    DumpMarelliMem START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 3072) or hex (e.g. 0xC00)\n        LENGTH = Number of bytes in decimal (e.g. 1024) or hex (e.g. 0x400)\n        FILENAME = Optional filename\n    DumpMem START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 8192) or hex (e.g. 0x2000)\n        LENGTH = Number of bytes in decimal (e.g. 65536) or hex (e.g. 0x10000)\n        FILENAME = Optional filename\n    DumpRam START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 8192) or hex (e.g. 0x2000)\n        LENGTH = Number of bytes in decimal (e.g. 65536) or hex (e.g. 0x10000)\n        FILENAME = Optional filename\n    DumpRBxMem START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 66560) or hex (e.g. 0x10400)\n        LENGTH = Number of bytes in decimal (e.g. 1024) or hex (e.g. 0x400)\n        FILENAME = Optional filename\n    DumpRom START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 8192) or hex (e.g. 0x2000)\n        LENGTH = Number of bytes in decimal (e.g. 65536) or hex (e.g. 0x10000)\n        FILENAME = Optional filename\n    FindLogins LOGIN\n        LOGIN = Known good login (0-65535)\n    GetSKC\n    GroupRead GROUP\n        GROUP = Group number (0-255)\n        (Group 0: Raw controller data)\n    LoadEeprom START FILENAME\n        START = Start address in decimal (e.g. 0) or hex (e.g. 0x0)\n        FILENAME = Name of file containing binary data to load into EEPROM\n    MapEeprom\n    ReadFaultCodes\n    ReadIdent\n    ReadEeprom ADDRESS\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ReadRAM ADDRESS\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ReadROM ADDRESS\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ReadSoftwareVersion\n    Reset\n    SetSoftwareCoding CODING WORKSHOP\n        CODING = Software coding in decimal (e.g. 4361) or hex (e.g. 0x1109)\n        WORKSHOP = Workshop code in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ToggleRB4Mode\n    WriteEdc15Eeprom ADDRESS1 VALUE1 [ADDRESS2 VALUE2 ... ADDRESSn VALUEn]\n        ADDRESS = EEPROM address in decimal (0-511) or hex (0x00-0x1FF)\n        VALUE = Value to be stored in decimal (0-255) or hex (0x00-0xFF)\n    WriteEeprom ADDRESS VALUE\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n        VALUE = Value in decimal (e.g. 138) or hex (e.g. 0x8A)\n    WriteRAM ADDRESS VALUE\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n        VALUE = Value in decimal (e.g. 138) or hex (e.g. 0x8A)\n\"\"\");\n    }\n\n    private string? _filename = null;\n}\n"
  },
  {
    "path": "Publish_Mac.ps1",
    "content": "dotnet publish kw1281test.csproj /p:PublishProfile=Win\ndotnet publish kw1281test.csproj /p:PublishProfile=Mac\ndotnet publish kw1281test.csproj /p:PublishProfile=Linux-Arm64\ndotnet publish kw1281test.csproj /p:PublishProfile=Linux-x64\n\n$Here = (Get-Location).Path\n$PublishSourceDir = \"$Here/bin/Release/net10.0/publish\"\n$GitHubDir = \"$Here/GitHub\"\n\nRemove-Item -Path $GitHubDir/*.*\n\n$ProjectXml = [xml](Get-Content ./kw1281test.csproj)\n$Version = $ProjectXml.Project.PropertyGroup.Version\n\n$WinExe = \"$PublishSourceDir\\Win\\kw1281test.exe\"\nCompress-Archive -Force -Path $WinExe -DestinationPath \"$GitHubDir/kw1281test_$($Version)_Win10.zip\"\n\n$MacZip = \"kw1281test_$($Version)_macOS.zip\"\nPush-Location -Path \"$PublishSourceDir/Mac/\"\nzip $MacZip kw1281test\nMove-Item -Force -Path $MacZip -Destination \"$GitHubDir/\"\nPop-Location\n\n$LinuxArmZip = \"kw1281test_$($Version)_Linux-Arm64.zip\"\nPush-Location -Path \"$PublishSourceDir/Linux-Arm64/\"\nzip $LinuxArmZip kw1281test\nMove-Item -Force -Path $LinuxArmZip -Destination \"$GitHubDir/\"\nPop-Location\n\n$LinuxZip = \"kw1281test_$($Version)_Linux-x64.zip\"\nPush-Location -Path \"$PublishSourceDir/Linux-x64/\"\nzip $LinuxZip kw1281test\nMove-Item -Force -Path $LinuxZip -Destination \"$GitHubDir/\"\nPop-Location\n\nStart-Process $GitHubDir\n"
  },
  {
    "path": "Publish_Win.ps1",
    "content": "dotnet publish kw1281test.csproj /p:PublishProfile=Win\ndotnet publish kw1281test.csproj /p:PublishProfile=Mac\ndotnet publish kw1281test.csproj /p:PublishProfile=Linux-Arm64\ndotnet publish kw1281test.csproj /p:PublishProfile=Linux-x64\n\n$PublishSourceDir = 'D:\\src\\kw1281test\\bin\\Release\\net10.0\\publish'\n$GitHubDir = 'D:\\src\\kw1281test\\GitHub'\n\nNew-Item -ItemType Directory -Force -Path $GitHubDir\nRemove-Item -Path $GitHubDir\\*.*\n\n$WinExe = \"$PublishSourceDir\\Win\\kw1281test.exe\"\n$Version = (Get-Item $WinExe).VersionInfo.ProductVersion\n\nCompress-Archive -Force -Path $WinExe -DestinationPath \"$GitHubDir\\kw1281test_$($Version)_Win10.zip\"\n\n$MacZip = \"kw1281test_$($Version)_macOS.zip\"\nPush-Location -Path \"$PublishSourceDir\\Mac\\\"\nwsl zip $MacZip kw1281test\nMove-Item -Force -Path $MacZip -Destination \"$GitHubDir\\\"\nPop-Location\n\n$LinuxArmZip = \"kw1281test_$($Version)_Linux-Arm64.zip\"\nPush-Location -Path \"$PublishSourceDir\\Linux-Arm64\\\"\nwsl zip $LinuxArmZip kw1281test\nMove-Item -Force -Path $LinuxArmZip -Destination \"$GitHubDir\\\"\nPop-Location\n\n$LinuxZip = \"kw1281test_$($Version)_Linux-x64.zip\"\nPush-Location -Path \"$PublishSourceDir\\Linux-x64\\\"\nwsl zip $LinuxZip kw1281test\nMove-Item -Force -Path $LinuxZip -Destination \"$GitHubDir\\\"\nPop-Location\n\nStart-Process .\\GitHub\n"
  },
  {
    "path": "README.md",
    "content": "# kw1281test\nVW KW1281 Protocol Test Tool\n\nThis tool can send some KW1281 (and a few KW2000) commands over a dumb serial->KKL or USB->KKL cable.\nIf you have a legacy Ross-Tech USB cable, you can probably use that cable by\ninstalling the Virtual COM Port drivers: https://www.ross-tech.com/vag-com/usb/virtual-com-port.php\nFunctionality includes reading/writing the EEPROMs of VW MKIV Golf/Jetta/Beetle/Passat instrument clusters and Comfort Control Modules, reading and clearing fault codes, changing the software coding of modules, performing an actuator test of various modules and retrieving the SAFE code of the Delco Premium V radio.\n\nThe tool is written in C#, targetting .NET 10.0 and runs under Windows 10/11 (most serial ports), macOS and Linux (macOS/Linux need an FTDI serial port and D2xx drivers). It may also run under\nWindows 10/11.\n\nYou can download a precompiled version for Windows, macOS and Linux from the Releases page: https://github.com/gmenounos/kw1281test/releases/\n\nOtherwise, here's how to build it yourself:\n\n##### Compiling the tool\n\n1. You will need the .NET Core SDK,\nwhich you can find here: https://dotnet.microsoft.com/download\n(Click on the \"Download .NET Core SDK\" link and follow the instructions) or Microsoft Visual Studio\n(free Community Edition here: https://visualstudio.microsoft.com/vs/community/)\n\n2. Download the source code: https://github.com/gmenounos/kw1281test/archive/master.zip\nand unzip it into a folder on your computer.\n\n3. Open up a command prompt on your computer and go into the folder where you unzipped\nthe source code. Type `dotnet build` to build the tool.\nOr, load up the project in Visual Studio and Ctrl-Shift-B.\n\n4. You can run the tool by typing `dotnet run`\n\n```\nUsage: KW1281Test PORT BAUD ADDRESS COMMAND [args]\n\nPORT = COM1|COM2|etc. (Windows)\n        /dev/ttyXXXX (Linux)\n        AABBCCDD (macOS/Linux FTDI cable serial number)\nBAUD = 10400|9600|etc.\nADDRESS = Controller address, e.g. 1 (ECU), 17 (cluster), 46 (CCM), 56 (radio)\nCOMMAND =\n    ActuatorTest\n    AdaptationRead CHANNEL [LOGIN]\n        CHANNEL = Channel number (0-99)\n        LOGIN = Optional login (0-65535)\n    AdaptationSave CHANNEL VALUE [LOGIN]\n        CHANNEL = Channel number (0-99)\n        VALUE = Channel value (0-65535)\n        LOGIN = Optional login (0-65535)\n    AdaptationTest CHANNEL VALUE [LOGIN]\n        CHANNEL = Channel number (0-99)\n        VALUE = Channel value (0-65535)\n        LOGIN = Optional login (0-65535)\n    AutoScan\n    BasicSetting GROUP\n        GROUP = Group number (0-255)\n        (Group 0: Raw controller data)\n    ClarionVWPremium4SafeCode\n    ClearFaultCodes\n    DelcoVWPremium5SafeCode\n    DumpEdc15Eeprom [FILENAME]\n        FILENAME = Optional filename\n    DumpEeprom START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 0) or hex (e.g. 0x0)\n        LENGTH = Number of bytes in decimal (e.g. 2048) or hex (e.g. 0x800)\n        FILENAME = Optional filename\n    DumpMarelliMem START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 3072) or hex (e.g. 0xC00)\n        LENGTH = Number of bytes in decimal (e.g. 1024) or hex (e.g. 0x400)\n        FILENAME = Optional filename\n    DumpMem START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 8192) or hex (e.g. 0x2000)\n        LENGTH = Number of bytes in decimal (e.g. 65536) or hex (e.g. 0x10000)\n        FILENAME = Optional filename\n    DumpRBxMem START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 66560) or hex (e.g. 0x10400)\n        LENGTH = Number of bytes in decimal (e.g. 1024) or hex (e.g. 0x400)\n        FILENAME = Optional filename\n    DumpRom START LENGTH [FILENAME]\n        START = Start address in decimal (e.g. 8192) or hex (e.g. 0x2000)\n        LENGTH = Number of bytes in decimal (e.g. 65536) or hex (e.g. 0x10000)\n    GetSKC\n    GroupRead GROUP\n        GROUP = Group number (0-255)\n        (Group 0: Raw controller data)\n    LoadEeprom START FILENAME\n        START = Start address in decimal (e.g. 0) or hex (e.g. 0x0)\n        FILENAME = Name of file containing binary data to load into EEPROM\n    MapEeprom\n    ReadFaultCodes\n    ReadIdent\n    ReadEeprom ADDRESS\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ReadRAM ADDRESS\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ReadROM ADDRESS\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ReadSoftwareVersion\n    Reset\n    SetSoftwareCoding CODING WORKSHOP\n        CODING = Software coding in decimal (e.g. 4361) or hex (e.g. 0x1109)\n        WORKSHOP = Workshop code in decimal (e.g. 4361) or hex (e.g. 0x1109)\n    ToggleRB4Mode\n    WriteEdc15Eeprom ADDRESS1 VALUE1 [ADDRESS2 VALUE2 ... ADDRESSn VALUEn]\n        ADDRESS = EEPROM address in decimal (0-511) or hex (0x00-0x1FF)\n        VALUE = Value to be stored in decimal (0-255) or hex (0x00-0xFF)\n    WriteEeprom ADDRESS VALUE\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n        VALUE = Value in decimal (e.g. 138) or hex (e.g. 0x8A)\n    WriteRAM ADDRESS VALUE\n        ADDRESS = Address in decimal (e.g. 4361) or hex (e.g. 0x1109)\n        VALUE = Value in decimal (e.g. 138) or hex (e.g. 0x8A)\n```\n\n##### Credits\n- Protocol Info: https://www.blafusel.de/obd/obd2_kw1281.html  \n- VW Radio Reverse Engineering Info: https://github.com/mnaberez/vwradio  \n- 6502bench SourceGen: https://6502bench.com/\n- EDC15 flashing info and seed/key algorithm: https://github.com/fjvva/ecu-tool\n- Contributions\n    - [IJskonijn](https://github.com/IJskonijn)\n    - [jpadie](https://github.com/jpadie)\n    - [kerekt](https://github.com/kerekt)\n    - [Olivier Fauchon](https://github.com/ofauchon)\n    - [Jonathan Klamroth](https://github.com/jonnykl)\n    - [Martin Sestak](https://github.com/poure-1)"
  },
  {
    "path": "Tester.cs",
    "content": "﻿using BitFab.KW1281Test.Cluster;\nusing BitFab.KW1281Test.EDC15;\nusing BitFab.KW1281Test.Interface;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.RegularExpressions;\nusing System.Threading;\n\nnamespace BitFab.KW1281Test;\n\ninternal class Tester\n{\n    private readonly IKwpCommon _kwpCommon;\n    private readonly IKW1281Dialog _kwp1281;\n    private readonly int _controllerAddress;\n\n\n    public Tester(IInterface @interface, int controllerAddress)\n    {\n        _kwpCommon = new KwpCommon(@interface);\n        _kwp1281 = new KW1281Dialog(_kwpCommon);\n        _controllerAddress = controllerAddress;\n    }\n\n    public ControllerInfo Kwp1281Wakeup(bool evenParityWakeup = false, bool failQuietly = false)\n    {\n        Log.WriteLine(\"Sending wakeup message\");\n\n        var kwpVersion = _kwpCommon.WakeUp((byte)_controllerAddress, evenParityWakeup, failQuietly);\n\n        if (kwpVersion != 1281)\n        {\n            throw new UnexpectedProtocolException(\"Expected KWP1281 protocol.\");\n        }\n\n        var ecuInfo = _kwp1281.Connect();\n        Log.WriteLine($\"ECU: {ecuInfo}\");\n        return ecuInfo;\n    }\n\n    public KW2000Dialog Kwp2000Wakeup(bool evenParityWakeup = false)\n    {\n        Log.WriteLine(\"Sending wakeup message\");\n\n        var kwpVersion = _kwpCommon!.WakeUp((byte)_controllerAddress, evenParityWakeup);\n\n        if (kwpVersion == 1281)\n        {\n            throw new UnexpectedProtocolException(\"Expected KWP2000 protocol.\");\n        }\n\n        var kwp2000 = new KW2000Dialog(_kwpCommon, (byte)_controllerAddress);\n\n        return kwp2000;\n    }\n\n    public void EndCommunication()\n    {\n        _kwp1281.EndCommunication();\n    }\n\n    // Begin top-level commands\n\n    public void ActuatorTest()\n    {\n        using KW1281KeepAlive keepAlive = new(_kwp1281);\n\n        ConsoleKeyInfo keyInfo;\n        do\n        {\n            var response = keepAlive.ActuatorTest(0x00);\n            if (response == null || response.ActuatorName == \"End\")\n            {\n                Log.WriteLine(\"End of test.\");\n                break;\n            }\n            Log.WriteLine($\"Actuator Test: {response.ActuatorName}\");\n\n            // Press any key to advance to next test or press Q to exit\n            Console.Write(\"Press 'N' to advance to next test or 'Q' to quit\");\n            do\n            {\n                keyInfo = Console.ReadKey(intercept: true);\n            } while (keyInfo.Key != ConsoleKey.N && keyInfo.Key != ConsoleKey.Q);\n            Console.WriteLine();\n        } while (keyInfo.Key != ConsoleKey.Q);\n    }\n\n    public void AdaptationRead(\n        byte channel,\n        ushort? login, int workshopCode)\n    {\n        if (login.HasValue)\n        {\n            _kwp1281.Login(login.Value, workshopCode);\n        }\n        _kwp1281.AdaptationRead(channel);\n    }\n\n    public void AdaptationSave(\n        byte channel, ushort channelValue,\n        ushort? login, int workshopCode)\n    {\n        if (login.HasValue)\n        {\n            _kwp1281.Login(login.Value, workshopCode);\n        }\n        _kwp1281.AdaptationSave(channel, channelValue, workshopCode);\n    }\n\n    public void AdaptationTest(\n        byte channel, ushort channelValue,\n        ushort? login, int workshopCode)\n    {\n        if (login.HasValue)\n        {\n            _kwp1281.Login(login.Value, workshopCode);\n        }\n        _kwp1281.AdaptationTest(channel, channelValue);\n    }\n\n    public void BasicSettingRead(byte groupNumber)\n    {\n        var succeeded = _kwp1281.GroupRead(groupNumber, useBasicSetting: true);\n    }\n\n    public void ClarionVWPremium4SafeCode()\n    {\n        if (_controllerAddress != (int)ControllerAddress.Radio)\n        {\n            Log.WriteLine(\"Only supported for radio address 56\");\n            return;\n        }\n\n        // Thanks to Mike Naberezny for this (https://github.com/mnaberez)\n        const byte readWriteSafeCode = 0xF0;\n        const byte read = 0x00;\n        _kwp1281.SendBlock(new List<byte> { readWriteSafeCode, read });\n\n        var block = _kwp1281.ReceiveBlocks().FirstOrDefault(b => !b.IsAckNak);\n\n        if (block == null)\n        {\n            Log.WriteLine(\"No response received from radio.\");\n        }\n        else if (block.Title != readWriteSafeCode)\n        {\n            Log.WriteLine(\n                $\"Unexpected response received from radio. Block title: ${block.Title:X2}\");\n        }\n        else\n        {\n            var safeCode = block.Body[0] * 256 + block.Body[1];\n            Log.WriteLine($\"Safe code: {safeCode:X4}\");\n        }\n    }\n\n    public void ClearFaultCodes()\n    {\n        var faultCodes = _kwp1281.ClearFaultCodes(_controllerAddress);\n\n        if (faultCodes != null)\n        {\n            if (faultCodes.Count == 0)\n            {\n                Log.WriteLine(\"Fault codes cleared.\");\n            }\n            else\n            {\n                Log.WriteLine(\"Fault codes:\");\n                foreach (var faultCode in faultCodes)\n                {\n                    Log.WriteLine($\"    {faultCode}\");\n                }\n            }\n        }\n        else\n        {\n            Log.WriteLine(\"Failed to clear fault codes.\");\n        }\n    }\n\n    public void DelcoVWPremium5SafeCode()\n    {\n        if (_controllerAddress != (int)ControllerAddress.RadioManufacturing)\n        {\n            Log.WriteLine(\"Only supported for radio manufacturing address 7C\");\n            return;\n        }\n\n        // Thanks to Mike Naberezny for this (https://github.com/mnaberez)\n        const string secret = \"DELCO\";\n        var code = (ushort)(secret[4] * 256 + secret[3]);\n        var workshopCode = secret[2] * 65536 + secret[1] * 256 + secret[0];\n\n        _kwp1281.Login(code, workshopCode);\n        var bytes = _kwp1281.ReadRomEeprom(0x0014, 2);\n        if (bytes != null)\n        {\n            Log.WriteLine($\"Safe code: {bytes[0]:X2}{bytes[1]:X2}\");\n        }\n        else\n        {\n            Log.WriteLine($\"Unable to determine Safe code.\");\n        }\n    }\n\n    public void DumpCcmRom(string? filename)\n    {\n        if (_controllerAddress != (int)ControllerAddress.CCM &&\n            _controllerAddress != (int)ControllerAddress.CentralLocking)\n        {\n            Log.WriteLine(\"Only supported for CCM and Central Locking\");\n            return;\n        }\n\n        UnlockControllerForEepromReadWrite();\n\n        var dumpFileName = filename ?? \"ccm_rom_dump.bin\";\n        const byte blockSize = 8;\n\n        Log.WriteLine($\"Saving CCM ROM to {dumpFileName}\");\n\n        var succeeded = true;\n        using (var fs = File.Create(dumpFileName, blockSize, FileOptions.WriteThrough))\n        {\n            for (var seg = 0; seg < 16; seg++)\n            {\n                for (var msb = 0; msb < 16; msb++)\n                {\n                    for (var lsb = 0; lsb < 256; lsb += blockSize)\n                    {\n                        var blockBytes = _kwp1281.ReadCcmRom((byte)seg, (byte)msb, (byte)lsb, blockSize);\n                        if (blockBytes == null)\n                        {\n                            blockBytes = Enumerable.Repeat((byte)0, blockSize).ToList();\n                            succeeded = false;\n                        }\n                        else if (blockBytes.Count < blockSize)\n                        {\n                            blockBytes.AddRange(Enumerable.Repeat((byte)0, blockSize - blockBytes.Count));\n                            succeeded = false;\n                        }\n\n                        fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                        fs.Flush();\n                    }\n                }\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    public void DumpClusterNecRom(string? filename)\n    {\n        if (_controllerAddress != (int)ControllerAddress.Cluster)\n        {\n            Log.WriteLine(\"Only supported for cluster\");\n            return;\n        }\n\n        var dumpFileName = filename ?? \"cluster_nec_rom_dump.bin\";\n        const byte blockSize = 16;\n\n        Log.WriteLine($\"Saving cluster NEC ROM to {dumpFileName}\");\n\n        bool succeeded = true;\n        using (var fs = File.Create(dumpFileName, blockSize, FileOptions.WriteThrough))\n        {\n            var cluster = new VdoCluster(_kwp1281);\n\n            for (int address = 0; address < 65536; address += blockSize)\n            {\n                var blockBytes = cluster.CustomReadNecRom((ushort)address, blockSize);\n                if (blockBytes == null)\n                {\n                    blockBytes = Enumerable.Repeat((byte)0, blockSize).ToList();\n                    succeeded = false;\n                }\n                else if (blockBytes.Count < blockSize)\n                {\n                    blockBytes.AddRange(Enumerable.Repeat((byte)0, blockSize - blockBytes.Count));\n                    succeeded = false;\n                }\n\n                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                fs.Flush();\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    public void FindLogins(ushort goodLogin, int workshopCode)\n    {\n        const int start = 0;\n        for (int login = start; login <= 65535; login++)\n        {\n            _kwp1281.Login(goodLogin, workshopCode);\n\n            try\n            {\n                Log.WriteLine($\"Trying {login:D5}\");\n                _kwp1281.Login((ushort)login, workshopCode);\n                Log.WriteLine($\"{login:D5} succeeded\");\n                continue;\n            }\n            catch(TimeoutException)\n            {\n                _kwp1281.SetDisconnected();\n                try\n                {\n                    Kwp1281Wakeup();\n                }\n                catch(InvalidOperationException)\n                {\n                    _kwp1281.SetDisconnected();\n                    Kwp1281Wakeup();\n                }\n            }\n        }\n    }\n\n    public byte[] ReadWriteEdc15Eeprom(\n        string? filename, List<KeyValuePair<ushort, byte>>? addressValuePairs = null)\n    {\n        _kwp1281.EndCommunication();\n\n        Thread.Sleep(1000);\n\n        // Now wake it up again, hopefully in KW2000 mode\n        _kwpCommon!.Interface.SetBaudRate(10400);\n        var kwpVersion = _kwpCommon.WakeUp((byte)_controllerAddress, evenParity: false);\n        if (kwpVersion < 2000)\n        {\n            throw new InvalidOperationException(\n                $\"Unable to wake up ECU in KW2000 mode. KW version: {kwpVersion}\");\n        }\n        Log.WriteLine($\"KW Version: {kwpVersion}\");\n\n        var edc15 = new Edc15VM(_kwpCommon, _controllerAddress);\n\n        var dumpFileName = filename ?? $\"EDC15_EEPROM.bin\";\n\n        return edc15.ReadWriteEeprom(dumpFileName, addressValuePairs);\n    }\n\n    public void DumpEeprom(uint address, uint length, string? filename)\n    {\n        switch (_controllerAddress)\n        {\n            case (int)ControllerAddress.Cluster:\n                ClusterDumpEeprom((ushort)address, (ushort)length, filename);\n                break;\n            case (int)ControllerAddress.CCM:\n            case (int)ControllerAddress.CentralElectric:\n            case (int)ControllerAddress.CentralLocking:\n                CcmDumpEeprom((ushort)address, (ushort)length, filename);\n                break;\n            default:\n                Log.WriteLine(\"Only supported for cluster, CCM, Central Locking and Central Electric\");\n                break;\n        }\n    }\n\n    public void DumpMarelliMem(\n        uint address, uint length, ControllerInfo ecuInfo, string? filename)\n    {\n        if (_controllerAddress != (int)ControllerAddress.Cluster)\n        {\n            Log.WriteLine(\"Only supported for clusters\");\n        }\n        else\n        {\n            ICluster cluster = new MarelliCluster(_kwp1281, ecuInfo.Text);\n            cluster.DumpEeprom(address, length, filename);\n        }\n    }\n\n    public void DumpMem(uint address, uint length, string? filename)\n    {\n        if (_controllerAddress != (int)ControllerAddress.Cluster)\n        {\n            Log.WriteLine(\"Only supported for cluster\");\n            return;\n        }\n\n        ClusterDumpMem(address, length, filename);\n    }\n\n    public void DumpRam(uint startAddr, uint length, string? filename)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        const int maxReadLength = 8;\n        bool succeeded = true;\n        string dumpFileName = filename ?? $\"ram_0x{startAddr:X4}.bin\";\n\n        using (var fs = File.Create(dumpFileName, maxReadLength, FileOptions.WriteThrough))\n        {\n            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)\n            {\n                var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);\n                var blockBytes = _kwp1281.ReadRam((ushort)addr, (byte)readLength);\n                if (blockBytes == null)\n                {\n                    blockBytes = Enumerable.Repeat((byte)0, readLength).ToList();\n                    succeeded = false;\n                }\n                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                fs.Flush();\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    public void DumpRom(uint startAddr, uint length, string? filename)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        const int maxReadLength = 8;\n        bool succeeded = true;\n        string dumpFileName = filename ?? $\"rom_0x{startAddr:X4}.bin\";\n\n        using (var fs = File.Create(dumpFileName, maxReadLength, FileOptions.WriteThrough))\n        {\n            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)\n            {\n                var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);\n                var blockBytes = _kwp1281.ReadRomEeprom((ushort)addr, (byte)readLength);\n                if (blockBytes == null)\n                {\n                    blockBytes = Enumerable.Repeat((byte)0, readLength).ToList();\n                    succeeded = false;\n                }\n                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                fs.Flush();\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    /// <summary>\n    /// Dumps the memory of a Bosch RB4/RB8 cluster to a file.\n    /// </summary>\n    /// <returns>The dump file name or null if the EEPROM was not dumped.</returns>\n    public string? DumpRBxMem(\n        uint address, uint length, string? filename,\n        bool evenParityWakeup = true)\n    {\n        if (_controllerAddress != (int)ControllerAddress.Cluster)\n        {\n            Log.WriteLine(\"Only supported for cluster (address 17)\");\n            return null;\n        }\n\n        var kwp2000 = Kwp2000Wakeup(evenParityWakeup);\n\n        var dumpFileName = filename ?? $\"RBx_0x{address:X6}_mem.bin\";\n\n        ICluster cluster = new BoschRBxCluster(kwp2000);\n        cluster.UnlockForEepromReadWrite();\n        cluster.DumpEeprom(address, length, dumpFileName);\n\n        return dumpFileName;\n    }\n\n    /// <summary>\n    /// Connects to the cluster and gets its unique ID. This is normally done by the radio in\n    /// order to detect if its been moved to a different vehicle.\n    /// </summary>\n    public void GetClusterId()\n    {\n#if false\n        if (_controllerAddress != 0x3F)\n        {\n            Log.WriteLine(\"Only supported for special cluster address $3F\");\n            return;\n        }\n#endif\n\n        _kwp1281.SendBlock(new List<byte>\n        {\n            (byte)BlockTitle.SecurityAccessMode1,\n\n            // The radio would send 4 random values for obfuscation, but the cluster ignores\n            // them so we'll just send 0's.\n            0x00, 0x00, 0x00, 0x00 // Challenge\n        });\n\n        var block = _kwp1281.ReceiveBlocks().FirstOrDefault(b => !b.IsAckNak);\n\n        if (block == null)\n        {\n            Log.WriteLine(\"No response received from cluster.\");\n        }\n        else if (block.Title != (byte)BlockTitle.SecurityAccessMode2)\n        {\n            Log.WriteLine(\n                $\"Unexpected response received from cluster. Block title: ${block.Title:X2}\");\n        }\n        else\n        {\n            (byte id1, byte id2) = DecodeClusterId(block.Body[0], block.Body[1], block.Body[2], block.Body[3]);\n            Log.WriteLine($\"Cluster Id: ${id1:X2} ${id2:X2}\");\n        }\n    }\n\n    public void GetSkc()\n    {\n        if (_controllerAddress is (int)ControllerAddress.Cluster or (int)ControllerAddress.Immobilizer)\n        {\n            var ecuInfo = Kwp1281Wakeup();\n\n            if (ecuInfo.Text.Contains(\"4B0920\") ||\n                ecuInfo.Text.Contains(\"4Z7920\") ||\n                ecuInfo.Text.Contains(\"8D0920\") ||\n                ecuInfo.Text.Contains(\"8Z0920\"))\n            {\n                var family = ecuInfo.Text[..2] switch\n                {\n                    \"8D\" => \"A4\",\n                    \"8Z\" => \"A2\",\n                    _ => \"C5\"\n                };\n\n                Log.WriteLine($\"Cluster is Audi {family}\");\n\n                var cluster = new AudiC5Cluster(_kwp1281);\n\n                cluster.UnlockForEepromReadWrite();\n                var dumpFileName = cluster.DumpEeprom(0, 0x800, $\"Audi{family}.bin\");\n\n                var buf = File.ReadAllBytes(dumpFileName);\n\n                var skc = Utils.GetShort(buf, 0x7E2);\n                var skc2 = Utils.GetShort(buf, 0x7E4);\n                var skc3 = Utils.GetShort(buf, 0x7E6);\n                if (skc != skc2 || skc != skc3)\n                {\n                    Log.WriteLine($\"Warning: redundant SKCs do not match: {skc:D5} {skc2:D5} {skc3:D5}\");\n                }\n                else\n                {\n                    Log.WriteLine($\"SKC: {skc:D5}\");\n                }\n            }\n            else if (\n                ecuInfo.Text.Contains(\"VDO\") ||\n                ecuInfo.Text.Contains(\"V2721446\") ||\n                ecuInfo.Text.Contains(\"V2823466\"))\n            {\n                var cluster = new VdoCluster(_kwp1281);\n                string[] partNumberGroups = FindAndParsePartNumber(ecuInfo.Text);\n                if (partNumberGroups.Length == 4)\n                {\n                    string dumpFileName;\n                    ushort startAddress;\n                    byte[] buf;\n                    ushort? skc;\n                    if (partNumberGroups[1] == \"919\") // Non-CAN\n                    {\n                        startAddress = 0x1FA;\n                        dumpFileName = ClusterDumpEeprom(startAddress, length: 6, filename: null);\n                        buf = File.ReadAllBytes(dumpFileName);\n                        skc = Utils.GetBcd(buf, 0);\n                        ushort skc2 = Utils.GetBcd(buf, 2);\n                        ushort skc3 = Utils.GetBcd(buf, 4);\n                        if (skc != skc2 || skc != skc3)\n                        {\n                            Log.WriteLine($\"Warning: redundant SKCs do not match: {skc:D5} {skc2:D5} {skc3:D5}\");\n                        }\n                    }\n                    else if (partNumberGroups[1] == \"920\") // CAN\n                    {\n                        startAddress = 0x90;\n                        dumpFileName = ClusterDumpEeprom(startAddress, length: 0x7C, filename: null);\n                        buf = File.ReadAllBytes(dumpFileName);\n                        skc = VdoCluster.GetSkc(buf, startAddress);\n                    }\n                    else\n                    {\n                        Log.WriteLine($\"Unknown cluster: {ecuInfo.Text}\");\n                        return;\n                    }\n\n                    if (skc.HasValue)\n                    {\n                        Log.WriteLine($\"SKC: {skc:D5}\");\n                    }\n                    else\n                    {\n                        Log.WriteLine($\"Unable to determine SKC.\");\n                    }\n                }\n                else\n                {\n                    Log.WriteLine($\"Unknown cluster: {ecuInfo.Text}\");\n                }\n            }\n            else if (ecuInfo.Text.Contains(\"RB4\"))\n            {\n                // Need to quit KWP1281 before switching to KWP2000\n                _kwp1281.EndCommunication();\n                Thread.Sleep(TimeSpan.FromSeconds(2));\n\n                var dumpFileName = DumpRBxMem(0x10046, 2, filename: null);\n                var buf = File.ReadAllBytes(dumpFileName!);\n                if (buf.Length == 2)\n                {\n                    var skc = Utils.GetShort(buf, 0);\n                    Log.WriteLine($\"SKC: {skc:D5}\");\n                }\n                else\n                {\n                    Log.WriteLine(\"Unable to read SKC. Cluster not in New mode (4)?\");\n                }\n            }\n            else if (ecuInfo.Text.Contains(\"RB8\"))\n            {\n                // Need to quit KWP1281 before switching to KWP2000\n                _kwp1281.EndCommunication();\n                Thread.Sleep(TimeSpan.FromSeconds(2));\n\n                var dumpFileName = DumpRBxMem(0x1040E, 2, filename: null);\n                var buf = File.ReadAllBytes(dumpFileName!);\n                var skc = Utils.GetShort(buf, 0);\n                Log.WriteLine($\"SKC: {skc:D5}\");\n            }\n            else if (ecuInfo.Text.Contains(\"M73\"))\n            {\n                ICluster cluster = new MarelliCluster(_kwp1281, ecuInfo.Text);\n\n                string dumpFileName = cluster.DumpEeprom(\n                    address: null, length: null, dumpFileName: null);\n                byte[] buf = File.ReadAllBytes(dumpFileName);\n                ushort? skc = MarelliCluster.GetSkc(buf);\n                if (skc.HasValue)\n                {\n                    Log.WriteLine($\"SKC: {skc:D5}\");\n                }\n                else\n                {\n                    Log.WriteLine($\"Unable to determine SKC for cluster: {ecuInfo.Text}\");\n                }\n            }\n            else if (ecuInfo.Text.Contains(\"BOO\") || ecuInfo.Text.Contains(\"MM0\"))\n            {\n                ICluster cluster = new MotometerBOOCluster(_kwp1281!);\n\n                cluster.UnlockForEepromReadWrite();\n\n                var dumpFileName = BOOClusterDumpEeprom(\n                    startAddress: 0, length: 0x10, filename: null);\n\n                var buf = File.ReadAllBytes(dumpFileName);\n                var skc = Utils.GetBcd(buf, 0x08);\n                Log.WriteLine($\"SKC: {skc:D5}\");\n            }\n            else if (ecuInfo.Text.Contains(\"VWZ3Z0\"))\n            {\n                // IMMO BOX 1 1H0 953 257 and 7M0 953 257 support based on sniffed communication.\n                // 7M0 953 257 can be both IMMO BOX 1 or IMMO BOX 2.\n\n                var blockBytes = _kwp1281.ReadRomEeprom(0x0190, 176);\n                if (blockBytes == null)\n                {\n                    Log.WriteLine(\"ROM read failed\");\n                    return;\n                }\n                else if (blockBytes.Count == 0)\n                {\n\n                    if (ecuInfo.Text.Contains(\"1H0\"))\n                    {\n                        Log.WriteLine(\"Failed to read SKC. Immo appears to be locked. You have to use an adapted key.\");\n                        return;\n                    }\n                    else if (ecuInfo.Text.Contains(\"6H0\") || ecuInfo.Text.Contains(\"7M0\"))\n                    {\n                        // This part adds IMMO BOX 2 experimental support (could not test this with real box).\n                        // Should work for 6H0 953 257 and 7M0 953 257\n\n                        Log.WriteLine(\"Trying to unlock IMMO BOX 2. This function is experimental and may not work...\");\n\n                        // Unlock ROM\n                        _kwp1281.SendBlock([0xCB, 0x5D, 0x3B, 0xD3, 0x8A]);\n\n                        // Send custom read command\n                        blockBytes = _kwp1281.ReadSecureImmoAccess([0x02, 0x00, 0x65, 0x34, 0x9D]);\n\n                        if (blockBytes == null || blockBytes.Count == 0)\n                        {\n                            Log.WriteLine(\"Failed to read SKC. Immo appears to be locked. You have to use an adapted key.\");\n                            return;\n                        }\n                    }\n                    else\n                    {\n                        Log.WriteLine(\"Failed to read SKC for non 1H0/6H0/7M0 ECU.\");\n                        return;\n                    }\n                }\n\n                var skc = Utils.GetShortBE(blockBytes.ToArray(), 1);\n                Log.WriteLine($\"SKC: {skc:D5}\");\n            }\n            else if (ecuInfo.Text.Contains(\"AGD\"))\n            {\n                Log.WriteLine($\"Unsupported Magneti Marelli AGD cluster: {ecuInfo.Text}\");\n            }\n            else\n            {\n                Log.WriteLine($\"Unsupported cluster: {ecuInfo.Text}\");\n            }\n        }\n        else if (_controllerAddress == (int)ControllerAddress.Ecu)\n        {\n            var ecuInfo = Kwp1281Wakeup();\n            var eeprom = ReadWriteEdc15Eeprom(filename: null);\n            Edc15VM.DisplayEepromInfo(eeprom);\n        }\n        else\n        {\n            Log.WriteLine(\n                \"GetSKC only supported for clusters (address 17), Immo boxes (address 25) and ECUs (address 1)\");\n        }\n    }\n\n    /// <summary>\n    /// Takes the info returned when connecting to the ECU, finds the ECU part number and\n    /// splits into its components. For example, if the ECU info is this:\n    ///     \"1J5920926CX   KOMBI+WEGFAHRSP VDO V01\"\n    /// Then the part number would be identified as \"1J5920926CX\", which would be split into\n    /// its 4 components: \"1J5\", \"920\", \"926\", \"CX\"\n    /// </summary>\n    /// <param name=\"ecuInfo\"></param>\n    /// <returns>A 4-element string array if the part number was found, otherwise an empty\n    /// string array.</returns>\n    internal static string[] FindAndParsePartNumber(string ecuInfo)\n    {\n        var match = Regex.Match(\n            ecuInfo,\n            \"\\\\b(\\\\d[a-zA-Z][0-9a-zA-Z])(9\\\\d{2})(\\\\d{3})([a-zA-Z]{0,2})\\\\b\");\n\n        if (match.Success)\n        {\n            return (match.Groups as IReadOnlyList<Group>).Skip(1).Select(g => g.Value).ToArray();\n        }\n        else\n        {\n            return Array.Empty<string>();\n        }\n    }\n\n    public void GroupRead(byte groupNumber)\n    {\n        var succeeded = _kwp1281.GroupRead(groupNumber);\n    }\n\n    public void LoadEeprom(uint address, string filename)\n    {\n        switch (_controllerAddress)\n        {\n            case (int)ControllerAddress.Cluster:\n                ClusterLoadEeprom((ushort)address, filename);\n                break;\n            case (int)ControllerAddress.CCM:\n            case (int)ControllerAddress.CentralElectric:\n            case (int)ControllerAddress.CentralLocking:\n                CcmLoadEeprom((ushort)address, filename);\n                break;\n            default:\n                Log.WriteLine(\"Only supported for cluster, CCM, Central Locking and Central Electric\");\n                break;\n        }\n    }\n\n    public void MapEeprom(string? filename)\n    {\n        switch (_controllerAddress)\n        {\n            case (int)ControllerAddress.Cluster:\n                ClusterMapEeprom(filename);\n                break;\n            case (int)ControllerAddress.CCM:\n            case (int)ControllerAddress.CentralElectric:\n            case (int)ControllerAddress.CentralLocking:\n                CcmMapEeprom(filename);\n                break;\n            default:\n                Log.WriteLine(\"Only supported for cluster, CCM, Central Locking and Central Electric\");\n                break;\n        }\n    }\n\n    public void ReadEeprom(uint address)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        var blockBytes = _kwp1281.ReadEeprom((ushort)address, 1);\n        if (blockBytes == null)\n        {\n            Log.WriteLine(\"EEPROM read failed\");\n        }\n        else\n        {\n            var value = blockBytes[0];\n            Log.WriteLine(\n                $\"Address {address} (${address:X4}): Value {value} (${value:X2})\");\n        }\n    }\n\n    public void ReadRam(uint address)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        var blockBytes = _kwp1281.ReadRam((ushort)address, 1);\n        if (blockBytes == null)\n        {\n            Log.WriteLine(\"RAM read failed\");\n        }\n        else\n        {\n            var value = blockBytes[0];\n            Log.WriteLine(\n                $\"Address {address} (${address:X4}): Value {value} (${value:X2})\");\n        }\n    }\n\n    public void ReadRom(uint address)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        var blockBytes = _kwp1281.ReadRomEeprom((ushort)address, 1);\n        if (blockBytes == null)\n        {\n            Log.WriteLine(\"ROM read failed\");\n        }\n        else\n        {\n            var value = blockBytes[0];\n            Log.WriteLine(\n                $\"Address {address} (${address:X4}): Value {value} (${value:X2})\");\n        }\n    }\n\n    public void ReadFaultCodes()\n    {\n        var faultCodes = _kwp1281.ReadFaultCodes();\n        if (faultCodes != null)\n        {\n            Log.WriteLine(\"Fault codes:\");\n            foreach (var faultCode in faultCodes)\n            {\n                Log.WriteLine($\"    {faultCode}\");\n            }\n        }\n    }\n\n    public void ReadIdent()\n    {\n        foreach (var identInfo in _kwp1281.ReadIdent())\n        {\n            Log.WriteLine($\"Ident: {identInfo}\");\n        }\n    }\n\n    public void ReadSoftwareVersion()\n    {\n        if (_controllerAddress == (int)ControllerAddress.Cluster)\n        {\n            var cluster = new VdoCluster(_kwp1281);\n            cluster.CustomReadSoftwareVersion();\n        }\n        else\n        {\n            Log.WriteLine(\"Only supported for cluster\");\n        }\n    }\n\n    public void Reset()\n    {\n        if (_controllerAddress == (int)ControllerAddress.Cluster)\n        {\n            var cluster = new VdoCluster(_kwp1281);\n            cluster.CustomReset();\n        }\n        else\n        {\n            Log.WriteLine(\"Only supported for cluster\");\n        }\n    }\n\n    public void SetSoftwareCoding(\n        int softwareCoding, int workshopCode)\n    {\n        var succeeded = _kwp1281.SetSoftwareCoding(_controllerAddress, softwareCoding, workshopCode);\n        if (succeeded)\n        {\n            Log.WriteLine(\"Software coding set.\");\n        }\n        else\n        {\n            Log.WriteLine(\"Failed to set software coding.\");\n        }\n    }\n\n    public void ToggleRB4Mode()\n    {\n        var kwp2000 = Kwp2000Wakeup(evenParityWakeup: true);\n\n        BoschRBxCluster cluster = new(kwp2000);\n        cluster.UnlockForEepromReadWrite();\n        cluster.ToggleRB4Mode();\n    }\n\n    public void WriteEeprom(uint address, byte value)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        _kwp1281.WriteEeprom((ushort)address, new List<byte> { value });\n    }\n\n    public void WriteRam(uint address, byte value)\n    {\n        switch (_controllerAddress)\n        {\n            case (int)ControllerAddress.Cluster:\n                ClusterWriteRam((ushort)address, value);\n                break;\n            default:\n                Log.WriteLine(\"Only supported for cluster\");\n                break;\n        }\n\n    }\n\n    // End top-level commands\n\n    private void ClusterWriteRam(ushort address, byte value)\n    {\n        // TODO: Verify cluster is VDO\n\n        var vdoCluster = new VdoCluster(_kwp1281);\n        if (!vdoCluster.RequiresSeedKey())\n        {\n            Log.WriteLine(\n                \"Cluster is unlocked for memory access. Skipping Seed/Key login.\");\n        }\n        else\n        {\n            var (isUnlocked, softwareVersion) = vdoCluster.Unlock();\n            if (!isUnlocked)\n            {\n                Log.WriteLine(\"Unknown cluster software version. Memory access will likely fail.\");\n            }\n            vdoCluster.SeedKeyAuthenticate(softwareVersion);\n        }\n\n        vdoCluster.WriteRam(address, value);\n    }\n\n    private string BOOClusterDumpEeprom(ushort startAddress, ushort length, string? filename)\n    {\n        var identInfo = _kwp1281.ReadIdent().First().ToString()\n            .Split(Environment.NewLine).First() // Sometimes ReadIdent() can return multiple lines\n            .Replace(' ', '_')\n            .Replace('.', '_')\n            .Replace(\":\", \"\");\n\n        var dumpFileName = filename ?? $\"{identInfo}_0x{startAddress:X4}_eeprom.bin\";\n        foreach (var c in Path.GetInvalidFileNameChars())\n        {\n            dumpFileName = dumpFileName.Replace(c, 'X');\n        }\n        foreach (var c in Path.GetInvalidPathChars())\n        {\n            dumpFileName = dumpFileName.Replace(c, 'X');\n        }\n\n        Log.WriteLine($\"Saving EEPROM dump to {dumpFileName}\");\n        DumpEeprom(startAddress, length, maxReadLength: 16, dumpFileName);\n        Log.WriteLine($\"Saved EEPROM dump to {dumpFileName}\");\n\n        return dumpFileName;\n    }\n\n    private string ClusterDumpEeprom(\n        ushort startAddress, ushort length, string? filename)\n    {\n        var identInfo = _kwp1281.ReadIdent().First().ToString()\n            .Split(Environment.NewLine).First() // Sometimes ReadIdent() can return multiple lines\n            .Replace(' ', '_').Replace(\":\", \"\");\n\n        ICluster cluster = new VdoCluster(_kwp1281);\n        cluster.UnlockForEepromReadWrite();\n\n        var dumpFileName = filename ?? $\"{identInfo}_0x{startAddress:X4}_eeprom.bin\";\n\n        Log.WriteLine($\"Saving EEPROM dump to {dumpFileName}\");\n        cluster.DumpEeprom(startAddress, length, dumpFileName);\n        Log.WriteLine($\"Saved EEPROM dump to {dumpFileName}\");\n\n        return dumpFileName;\n    }\n\n    private void CcmMapEeprom(string? filename)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        var bytes = new List<byte>();\n        const byte blockSize = 1;\n        for (int addr = 0; addr <= 65535; addr += blockSize)\n        {\n            var blockBytes = _kwp1281.ReadEeprom((ushort)addr, blockSize);\n            blockBytes = Enumerable.Repeat(\n                blockBytes == null ? (byte)0 : (byte)0xFF,\n                blockSize).ToList();\n            bytes.AddRange(blockBytes);\n        }\n        var dumpFileName = filename ?? \"ccm_eeprom_map.bin\";\n        Log.WriteLine($\"Saving EEPROM map to {dumpFileName}\");\n        File.WriteAllBytes(dumpFileName, bytes.ToArray());\n    }\n\n    private void ClusterMapEeprom(string? filename)\n    {\n        var cluster = new VdoCluster(_kwp1281);\n\n        var map = cluster.MapEeprom();\n\n        var mapFileName = filename ?? \"eeprom_map.bin\";\n        Log.WriteLine($\"Saving EEPROM map to {mapFileName}\");\n        File.WriteAllBytes(mapFileName, map.ToArray());\n    }\n\n    private void CcmDumpEeprom(ushort startAddress, ushort length, string? filename)\n    {\n        UnlockControllerForEepromReadWrite();\n\n        var dumpFileName = filename ?? $\"ccm_eeprom_0x{startAddress:X4}.bin\";\n\n        Log.WriteLine($\"Saving EEPROM dump to {dumpFileName}\");\n        DumpEeprom(startAddress, length, maxReadLength: 8, dumpFileName);\n        Log.WriteLine($\"Saved EEPROM dump to {dumpFileName}\");\n    }\n\n    private void UnlockControllerForEepromReadWrite()\n    {\n        switch ((ControllerAddress)_controllerAddress)\n        {\n            case ControllerAddress.CCM:\n            case ControllerAddress.CentralLocking:\n                _kwp1281.Login(\n                    code: 19283,\n                    workshopCode: 222); // This is what VDS-PRO uses\n                break;\n\n            case ControllerAddress.CentralElectric:\n                _kwp1281.Login(\n                    code: 21318,\n                    workshopCode: 222); // This is what VDS-PRO uses\n                break;\n\n            case ControllerAddress.Cluster:\n                // TODO:UnlockCluster() is only needed for EEPROM read, not memory read\n                var vdoCluster = new VdoCluster(_kwp1281);\n                var (isUnlocked, softwareVersion) = vdoCluster.Unlock();\n                if (!isUnlocked)\n                {\n                    Log.WriteLine(\"Unknown cluster software version. EEPROM access will likely fail.\");\n                }\n\n                if (!vdoCluster.RequiresSeedKey())\n                {\n                    Log.WriteLine(\n                        \"Cluster is unlocked for ROM/EEPROM access. Skipping Seed/Key login.\");\n                    return;\n                }\n\n                vdoCluster.SeedKeyAuthenticate(softwareVersion);\n                if (vdoCluster.RequiresSeedKey())\n                {\n                    Log.WriteLine(\"Failed to unlock cluster.\");\n                }\n                else\n                {\n                    Log.WriteLine(\"Cluster is unlocked for ROM/EEPROM access.\");\n                }\n                break;\n        }\n    }\n\n    private void DumpEeprom(\n        ushort startAddr, uint length, byte maxReadLength, string fileName)\n    {\n        bool succeeded = true;\n\n        using (var fs = File.Create(fileName, maxReadLength, FileOptions.WriteThrough))\n        {\n            for (uint addr = startAddr; addr < (startAddr + length); addr += maxReadLength)\n            {\n                var readLength = (byte)Math.Min(startAddr + length - addr, maxReadLength);\n                var blockBytes = _kwp1281.ReadEeprom((ushort)addr, (byte)readLength) ?? [];\n                if (blockBytes.Count < readLength)\n                {\n                    blockBytes.AddRange(Enumerable.Repeat((byte)0, readLength - blockBytes.Count));\n                    succeeded = false;\n                }\n                fs.Write(blockBytes.ToArray(), 0, blockBytes.Count);\n                fs.Flush();\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine();\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine(\"*** Warning: Some bytes could not be read and were replaced with 0 ***\");\n            Log.WriteLine(\"**********************************************************************\");\n            Log.WriteLine();\n        }\n    }\n\n    private void WriteEeprom(\n        ushort startAddr, byte[] bytes, uint maxWriteLength)\n    {\n        var succeeded = true;\n        var length = bytes.Length;\n        for (uint addr = startAddr; addr < (startAddr + length); addr += maxWriteLength)\n        {\n            var writeLength = (byte)Math.Min(startAddr + length - addr, maxWriteLength);\n            if (!_kwp1281.WriteEeprom(\n                (ushort)addr,\n                bytes.Skip((int)(addr - startAddr)).Take(writeLength).ToList()))\n            {\n                succeeded = false;\n            }\n        }\n\n        if (!succeeded)\n        {\n            Log.WriteLine(\"EEPROM write failed. You should probably try again.\");\n        }\n    }\n\n    private void CcmLoadEeprom(ushort address, string filename)\n    {\n        _ = _kwp1281.ReadIdent();\n\n        UnlockControllerForEepromReadWrite();\n\n        if (!File.Exists(filename))\n        {\n            Log.WriteLine($\"File {filename} does not exist.\");\n            return;\n        }\n\n        Log.WriteLine($\"Reading {filename}\");\n        var bytes = File.ReadAllBytes(filename);\n\n        Log.WriteLine(\"Writing to cluster...\");\n        WriteEeprom(address, bytes, 8);\n    }\n\n    private void ClusterLoadEeprom(ushort address, string filename)\n    {\n        _ = _kwp1281.ReadIdent();\n\n        UnlockControllerForEepromReadWrite();\n\n        if (!File.Exists(filename))\n        {\n            Log.WriteLine($\"File {filename} does not exist.\");\n            return;\n        }\n\n        Log.WriteLine($\"Reading {filename}\");\n        var bytes = File.ReadAllBytes(filename);\n\n        Log.WriteLine(\"Writing to cluster...\");\n        WriteEeprom(address, bytes, 16);\n    }\n\n    private void ClusterDumpMem(uint startAddress, uint length, string? filename)\n    {\n        // TODO: Verify cluster is VDO\n\n        var vdoCluster = new VdoCluster(_kwp1281);\n        if (!vdoCluster.RequiresSeedKey())\n        {\n            Log.WriteLine(\n                \"Cluster is unlocked for memory access. Skipping Seed/Key login.\");\n        }\n        else\n        {\n            var (isUnlocked, softwareVersion) = vdoCluster.Unlock();\n            if (!isUnlocked)\n            {\n                Log.WriteLine(\"Unknown cluster software version. Memory access will likely fail.\");\n            }\n            vdoCluster.SeedKeyAuthenticate(softwareVersion);\n        }\n\n        var dumpFileName = filename ?? $\"cluster_mem_0x{startAddress:X6}.bin\";\n        Log.WriteLine($\"Saving memory dump to {dumpFileName}\");\n\n        vdoCluster.DumpMem(dumpFileName, startAddress, length);\n\n        Log.WriteLine($\"Saved memory dump to {dumpFileName}\");\n    }\n\n    private static (byte, byte) DecodeClusterId(byte b1, byte b2, byte b3, byte b4)\n    {\n        // For obfuscation, the cluster adds the values below, so we need to subtract them:\n        bool carry = true;\n        (b1, carry) = Utils.SubtractWithCarry(b1, 0xE7, carry);\n        (b2, carry) = Utils.SubtractWithCarry(b2, 0xBD, carry);\n        (b3, carry) = Utils.SubtractWithCarry(b3, 0x18, carry);\n        (b4, carry) = Utils.SubtractWithCarry(b4, 0x00, carry);\n\n        b1 ^= b3;\n        b2 ^= b4;\n\n        // Count the number of 0 bits in b1 and b2\n\n        byte zeroCount = 0;\n        for (int i = 0; i < 8; i++)\n        {\n            if (((b1 >> i) & 1) == 0)\n            {\n                zeroCount++;\n            }\n            if (((b2 >> i) & 1) == 0)\n            {\n                zeroCount++;\n            }\n        }\n\n        // Right-rotate b3 and b4 zeroCount times:\n        for (int i = 0; i < zeroCount; i++)\n        {\n            carry = (b4 & 1) != 0;\n            (b3, carry) = Utils.RightRotate(b3, carry);\n            (b4, carry) = Utils.RightRotate(b4, carry);\n        }\n\n        b1 ^= b3;\n        b2 ^= b4;\n\n        return (b1, b2);\n    }\n}\n"
  },
  {
    "path": "Tests/BitFab.KW1281Test.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n    <PackageReference Include=\"MSTest.TestAdapter\" Version=\"4.0.2\" />\n    <PackageReference Include=\"MSTest.TestFramework\" Version=\"4.0.2\" />\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Shouldly\" Version=\"4.3.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\kw1281test.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"Cluster\\06032.bin\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Cluster\\AUZ_03997.bin\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"Cluster\\VWZ_02755.bin\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "Tests/Cluster/MarelliClusterTests.cs",
    "content": "using BitFab.KW1281Test.Cluster;\n\nnamespace BitFab.KW1281Test.Tests.Cluster;\n\n[TestClass]\npublic class MarelliClusterTests\n{\n    [TestMethod]\n    [DataRow(\"VWZ_02755.bin\", 02755)]    // ImmoId starts with \"VWZ\"\n    [DataRow(\"AUZ_03997.bin\", 03997)]    // ImmoId starts with \"AUZ\"\n    [DataRow(\"06032.bin\", 06032)]        // No ImmoId but key count pattern\n    public void GetSkc_ReturnsCorrectSkc(\n        string fileName, int expectedSkc)\n    {\n        var eeprom = File.ReadAllBytes($\"Cluster/{fileName}\");\n        var skc = MarelliCluster.GetSkc(eeprom);\n        Assert.AreEqual((ushort?)expectedSkc, skc);\n    }\n    \n    [TestMethod]\n    public void GetSkc_NoImmoIdAndNoKeyCountPattern_ReturnsNull()\n    {\n        var eeprom = new byte[1024];\n        var skc = MarelliCluster.GetSkc(eeprom);\n        Assert.IsNull(skc);\n    }\n}"
  },
  {
    "path": "Tests/Cluster/VdoClusterTests.cs",
    "content": "﻿using BitFab.KW1281Test.Cluster;\nusing Shouldly;\n\nnamespace BitFab.KW1281Test.Tests.Cluster\n{\n    [TestClass]\n    public class VdoClusterTests\n    {\n        [TestMethod]\n        [DataRow(\"MPV300LL 00.90\", (byte[])[0x3F, 0x38, 0x43, 0x38])]\n        [DataRow(\"MPV300LL 02.00\", (byte[])[0x3B, 0x47, 0x03, 0x02])]\n        [DataRow(\"MPV300LL 03.00\", (byte[])[0x43, 0x43, 0x43, 0x39])]\n        [DataRow(\"MPV300LL 04.00\", (byte[])[0x38, 0x47, 0x34, 0x3A])]\n        [DataRow(\"MPV300MH 01.00\", null)]\n        [DataRow(\"MPV500LL 00.90\", (byte[])[0x3F, 0x38, 0x43, 0x38])]\n        [DataRow(\"MPV501MH 01.00\", (byte[])[0x38, 0x47, 0x34, 0x3A])]\n        [DataRow(\"MPV501MH 06.00\", null)]\n        [DataRow(\"SS5500LM 01.00\", (byte[])[0x40, 0x39, 0x39, 0x38])]\n        [DataRow(\"SS5501LM 01.00\", (byte[])[0x3C, 0x34, 0x47, 0x35])]\n        [DataRow(\"SS5501LM 00.80\", (byte[])[0x36, 0x3B, 0x36, 0x3D])]\n        [DataRow(\"SS5501ML 01.00\", (byte[])[0x3C, 0x34, 0x47, 0x35])]\n        [DataRow(\"SS5501ML 00.80\", (byte[])[0x36, 0x3B, 0x36, 0x3D])]\n        [DataRow(\"S599CAA  00.80\", (byte[])[0x3D, 0x39, 0x3B, 0x35])]\n        [DataRow(\"VAT500LL 01.00\", (byte[])[0x01, 0x04, 0x3D, 0x35])]\n        [DataRow(\"VAT500LL 01.20\", (byte[])[0x01, 0x04, 0x3D, 0x35])]\n        [DataRow(\"VAT500MH 01.10\", (byte[])[0x01, 0x04, 0x3D, 0x35])]\n        [DataRow(\"VBK700LL 01.00\", (byte[])[0x3A, 0x39, 0x31, 0x43])]\n        [DataRow(\"VBK700LL 00.96\", (byte[])[0x3A, 0x39, 0x31, 0x43])]\n        [DataRow(\"VBKX00MH 01.00\", (byte[])[0x3A, 0x39, 0x31, 0x43])]\n        [DataRow(\"VWK501LL 01.00\", (byte[])[0x36, 0x3D, 0x3E, 0x47])]\n        [DataRow(\"VWK501MH 01.10\", (byte[])[0x39, 0x34, 0x34, 0x40])]\n        [DataRow(\"VWK503MH 09.00\", (byte[])[0x3E, 0x35, 0x3D, 0x3A])]\n        [DataRow(\"VWK503LL 09.00\", (byte[])[0x3E, 0x35, 0x3D, 0x3A])]\n        [DataRow(\"VSQX01LM 01.00\", (byte[])[0x31, 0x39, 0x34, 0x46])]\n        [DataRow(\"VSQX01LM 01.10\", (byte[])[0x43, 0x43, 0x3D, 0x37])]\n        [DataRow(\"VSQX01LM 01.20\", (byte[])[0x3D, 0x36, 0x40, 0x36])]\n        [DataRow(\"VT5X02LL 09.40\", (byte[])[0x36, 0x3F, 0x45, 0x42])]\n        [DataRow(\"VT5X02LL 09.00\", (byte[])[0x38, 0x39, 0x3A, 0x47])]\n        [DataRow(\"VQMJ06LM 09.00\", (byte[])[0x35, 0x3D, 0x47, 0x3E])]\n        [DataRow(\"VQMJ07LM 09.00\", (byte[])[0x34, 0x3F, 0x43, 0x39])]\n        [DataRow(\"VQMJ07LM 08.40\", (byte[])[0x34, 0x3F, 0x43, 0x39])]\n        [DataRow(\"VKQ501HH 09.00\", (byte[])[0x34, 0x3F, 0x43, 0x39])]\n        public void GetClusterUnlockCodes_ReturnsCorrectCode(\n            string softwareVersion, byte[] unlockCode)\n        {\n            var actualUnlockCodes = VdoCluster.GetClusterUnlockCodes(softwareVersion);\n            if (unlockCode == null)\n            {\n                actualUnlockCodes.Length.ShouldBeGreaterThan(1);\n            }\n            else\n            {\n                actualUnlockCodes.Length.ShouldBe(1);\n                actualUnlockCodes[0].ShouldBe(unlockCode);\n            }\n        }\n\n        [TestMethod]\n        [DataRow((byte[])[0x01, 0x04, 0x3D, 0x35])]\n        [DataRow((byte[])[0x31, 0x39, 0x34, 0x46])]\n        [DataRow((byte[])[0x34, 0x3F, 0x43, 0x39])]\n        [DataRow((byte[])[0x35, 0x3D, 0x47, 0x3E])]\n        [DataRow((byte[])[0x36, 0x3B, 0x36, 0x3D])]\n        [DataRow((byte[])[0x36, 0x3D, 0x3E, 0x47])]\n        [DataRow((byte[])[0x36, 0x3F, 0x45, 0x42])]\n        [DataRow((byte[])[0x38, 0x39, 0x3A, 0x47])]\n        [DataRow((byte[])[0x38, 0x47, 0x34, 0x3A])]\n        [DataRow((byte[])[0x39, 0x34, 0x34, 0x40])]\n        [DataRow((byte[])[0x3A, 0x39, 0x31, 0x43])]\n        [DataRow((byte[])[0x3B, 0x47, 0x03, 0x02])]\n        [DataRow((byte[])[0x3C, 0x34, 0x47, 0x35])]\n        [DataRow((byte[])[0x3D, 0x36, 0x40, 0x36])]\n        [DataRow((byte[])[0x3D, 0x39, 0x3B, 0x35])]\n        [DataRow((byte[])[0x3E, 0x35, 0x3D, 0x3A])]\n        [DataRow((byte[])[0x3F, 0x38, 0x43, 0x38])]\n        [DataRow((byte[])[0x40, 0x39, 0x39, 0x38])]\n        [DataRow((byte[])[0x43, 0x43, 0x3D, 0x37])]\n        [DataRow((byte[])[0x43, 0x43, 0x43, 0x39])]\n        public void ClusterUnlockCodes_ContainsKnownCodes(byte[] unlockCode)\n        {\n            foreach (var code in VdoCluster.ClusterUnlockCodes)\n            {\n                if (code.SequenceEqual(unlockCode))\n                {\n                    return; // Found the code, no need to check further\n                }\n            }\n            Assert.Fail($\"Unlock code {BitConverter.ToString(unlockCode)} not found in known codes.\");\n        }\n\n        [TestMethod]\n        public void ClusterUnlockCodes_ContainsNoDuplicates()\n        {\n            var seenCodes = new HashSet<string>();\n\n            foreach (var code in VdoCluster.ClusterUnlockCodes)\n            {\n                var codeString = BitConverter.ToString(code);\n                if (seenCodes.Contains(codeString))\n                {\n                    Assert.Fail($\"Duplicate unlock code found: {codeString}\");\n                }\n                seenCodes.Add(codeString);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Tests/GlobalUsings.cs",
    "content": "global using Microsoft.VisualStudio.TestTools.UnitTesting;\nusing System.Diagnostics.CodeAnalysis;\n\n[assembly: Parallelize]\n[assembly: ExcludeFromCodeCoverage(Justification = \"Unit tests\")]\n"
  },
  {
    "path": "Tests/ProgramTests.cs",
    "content": "namespace BitFab.KW1281Test.Tests;\n\n[TestClass]\npublic class ProgramTests\n{\n    [TestMethod]\n    public void ParseAddressesAndValues_NumberOfArgumentsIsOdd_ReturnsFalse()\n    {\n        var returnValue = Program.ParseAddressesAndValues([\"1\"], out var addressValuePairs);\n        \n        Assert.IsFalse(returnValue);\n    }\n\n    [TestMethod]\n    public void ParseAddressesAndValues_ValidArguments_ReturnsList()\n    {\n        var returnValue = Program.ParseAddressesAndValues(\n            [\"1\", \"25\", \"17\", \"42\"], out var addressValuePairs);\n        \n        Assert.IsTrue(returnValue);\n        Assert.HasCount(2, addressValuePairs);\n        Assert.AreEqual(new KeyValuePair<ushort, byte>(1, 25), addressValuePairs[0]);\n        Assert.AreEqual(new KeyValuePair<ushort, byte>(17, 42), addressValuePairs[1]);\n    }\n    \n    [TestMethod]\n    public void ParseAddressesAndValues_AddressTooLarge_ReturnsFalse()\n    {\n        var returnValue = Program.ParseAddressesAndValues(\n            [\"512\", \"25\", \"17\", \"42\"], out var addressValuePairs);\n        \n        Assert.IsFalse(returnValue);\n    }\n    \n    [TestMethod]\n    public void ParseAddressesAndValues_ValueTooLarge_ReturnsFalse()\n    {\n        var returnValue = Program.ParseAddressesAndValues(\n            [\"1\", \"25\", \"17\", \"256\"], out var addressValuePairs);\n        \n        Assert.IsFalse(returnValue);\n    }\n}"
  },
  {
    "path": "Tests/TesterTests.cs",
    "content": "namespace BitFab.KW1281Test.Tests\n{\n    [TestClass]\n    public class TesterTests\n    {\n        [TestMethod]\n        [DataRow(\"Nothing to see here\")]\n        [DataRow(\"1J0920927   KOMBI+WEGFAHRSP VDO V01\", \"1J0\", \"920\", \"927\", \"\")] // No alpha suffix\n        [DataRow(\"1J5920926C   KOMBI+WEGFAHRSP VDO V01\", \"1J5\", \"920\", \"926\", \"C\")] // 1 letter suffix\n        [DataRow(\"1J5920926CX   KOMBI+WEGFAHRSP VDO V01\", \"1J5\", \"920\", \"926\", \"CX\")] // 2 letter suffix\n        [DataRow(\"1JE920827   KOMBI+WEGFAHRSP VDO V01\", \"1JE\", \"920\", \"827\", \"\")] // 1st group ends in a letter\n        public void FindAndParsePartNumber_ReturnsExpectedGroups(\n            string ecuInfo, params string[] expectedGroups)\n        {\n            string[] actualGroups = Tester.FindAndParsePartNumber(ecuInfo);\n\n            Assert.HasCount(expectedGroups.Length, actualGroups);\n            for (var i = 0; i < expectedGroups.Length; i++)\n            {\n                Assert.AreEqual(expectedGroups[i], actualGroups[i]);\n            }\n        }\n    }\n}"
  },
  {
    "path": "Tests/UtilsTests.cs",
    "content": "﻿using Shouldly;\n\nnamespace BitFab.KW1281Test.Tests;\n\n[TestClass]\npublic class UtilsTests\n{\n    [TestMethod]\n    [DataRow(new byte[0], \"\")]\n    [DataRow(new byte[] { 31 }, \"$1F\")]\n    [DataRow(new byte[] { 32 }, \" \")]\n    [DataRow(new byte[] { 126 }, \"~\")]\n    [DataRow(new byte[] { 127 }, \"$7F\")]\n    [DataRow(new byte[] { (byte)'A', (byte)'B' }, \"AB\")]\n    [DataRow(new byte[] { (byte)'A', (byte)'B', 0x07 }, \"AB $07\")]\n    [DataRow(new byte[] { (byte)'A', (byte)'B', 0x07, 0x09 }, \"AB $07 $09\")]\n    [DataRow(new byte[] { (byte)'A', (byte)'B', 0x07, 0x09, (byte)'C', (byte)'D' }, \"AB $07 $09 CD\")]\n    [DataRow(new byte[] { 0x03, 0x04, (byte)'A', (byte)'B', 0x05, 0x06 }, \"$03 $04 AB $05 $06\")]\n    public void DumpMixedContent(byte[] content, string expectedDump)\n    {\n        var actualDump = Utils.DumpMixedContent(content);\n        actualDump.ShouldBe(expectedDump);\n    }\n}\n"
  },
  {
    "path": "UnableToProceedException.cs",
    "content": "﻿using System;\n\nnamespace BitFab.KW1281Test\n{\n    class UnableToProceedException : Exception\n    {\n    }\n}\n"
  },
  {
    "path": "UnexpectedProtocolException.cs",
    "content": "﻿using System;\n\nnamespace BitFab.KW1281Test\n{\n    [Serializable]\n    internal class UnexpectedProtocolException : Exception\n    {\n        public UnexpectedProtocolException()\n        {\n        }\n\n        public UnexpectedProtocolException(string? message) : base(message)\n        {\n        }\n\n        public UnexpectedProtocolException(string? message, Exception? innerException) : base(message, innerException)\n        {\n        }\n    }\n}"
  },
  {
    "path": "Utils.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Text;\n\nnamespace BitFab.KW1281Test;\n\ninternal static class Utils\n{\n    public static string Dump(IEnumerable<byte> bytes)\n    {\n        var sb = new StringBuilder();\n        foreach (var b in bytes)\n        {\n            sb.Append($\" {b:X2}\");\n        }\n        return sb.ToString();\n    }\n\n    // TODO: Merge with Dump()\n    public static string DumpBytes(IEnumerable<byte> bytes)\n    {\n        var sb = new StringBuilder();\n        foreach (var b in bytes)\n        {\n            sb.Append($\"${b:X2} \");\n        }\n        return sb.ToString();\n    }\n\n    public static string DumpDecimal(IEnumerable<byte> bytes)\n    {\n        var sb = new StringBuilder();\n        foreach (var b in bytes)\n        {\n            sb.Append($\" {b:D3}\");\n        }\n        return sb.ToString();\n    }\n\n    public static string DumpAscii(IEnumerable<byte> bytes)\n    {\n        var sb = new StringBuilder();\n        foreach (var b in bytes)\n        {\n            sb.Append((char)b);\n        }\n        return sb.ToString();\n    }\n\n    public static string DumpMixedContent(IEnumerable<byte> content)\n    {\n        char mode = '?';\n        var sb = new StringBuilder();\n        foreach (var b in content)\n        {\n            if (b is >= 32 and <= 126)\n            {\n                if (mode == 'X')\n                {\n                    sb.Append(' ');\n                }\n                mode = 'A';\n\n                sb.Append((char)b);\n            }\n            else\n            {\n                if (mode != '?')\n                {\n                    sb.Append(' ');\n                }\n                mode = 'X';\n\n                sb.Append($\"${b:X2}\");\n            }\n        }\n        return sb.ToString();\n    }\n\n    public static uint ParseUint(string numberString)\n    {\n        uint number;\n\n        if (numberString.StartsWith('$'))\n        {\n            number = uint.Parse(numberString[1..], NumberStyles.HexNumber);\n        }\n        else if (numberString.ToLower().StartsWith(\"0x\"))\n        {\n            number = uint.Parse(numberString[2..], NumberStyles.HexNumber);\n        }\n        else\n        {\n            number = uint.Parse(numberString);\n        }\n\n        return number;\n    }\n\n    /// <summary>\n    /// Little-Endian\n    /// </summary>\n    public static ushort GetShort(ReadOnlySpan<byte> buf, int offset)\n    {\n        return (ushort)(buf[offset] + buf[offset + 1] * 256);\n    }\n\n    /// <summary>\n    /// Big-Endian version of GetShort\n    /// </summary>\n    public static ushort GetShortBE(byte[] buf, int offset)\n    {\n        return (ushort)(buf[offset] * 256 + buf[offset + 1]);\n    }\n\n    /// <summary>\n    /// Little-Endian Binary Coded Decimal\n    /// </summary>\n    public static ushort GetBcd(byte[] buf, int offset)\n    {\n        var binary = GetShort(buf, offset);\n\n        ushort bcd = (ushort)\n            (\n                (binary >> 12) * 1000 +\n                ((binary >> 8) & 0x0F) * 100 +\n                ((binary >> 4) & 0x0F) * 10 +\n                (binary & 0x0F)\n            );\n\n        return bcd;\n    }\n\n    /// <summary>\n    /// Little-Endian\n    /// </summary>\n    public static byte[] GetBytes(uint value)\n    {\n        var bytes = new byte[4];\n\n        bytes[0] = (byte)(value & 0xFF);\n        value >>= 8;\n        bytes[1] = (byte)(value & 0xFF);\n        value >>= 8;\n        bytes[2] = (byte)(value & 0xFF);\n        value >>= 8;\n        bytes[3] = (byte)(value);\n\n        return bytes;\n    }\n\n    /// <summary>\n    /// Rotate a byte right.\n    /// </summary>\n    public static (byte result, bool carry) RightRotate(\n        byte value, bool carry)\n    {\n        var newCarry = (value & 0x01) != 0;\n        if (carry)\n        {\n            return ((byte)((value >> 1) | 0x80), newCarry);\n        }\n        else\n        {\n            return ((byte)(value >> 1), newCarry);\n        }\n    }\n\n    /// <summary>\n    /// Left-Rotate a value.\n    /// </summary>\n    public static (byte result, bool carry) LeftRotate(\n        byte value, bool carry)\n    {\n        var newCarry = (value & 0x80) != 0;\n        if (carry)\n        {\n            return ((byte)((value << 1) | 0x01), newCarry);\n        }\n        else\n        {\n            return ((byte)(value << 1), newCarry);\n        }\n    }\n\n    public static (byte result, bool carry) SubtractWithCarry(\n        byte minuend, byte subtrahend, bool carry)\n    {\n        int result = minuend - subtrahend - (carry ? 0 : 1);\n        carry = !(result < 0);\n\n        return ((byte)result, carry);\n    }\n\n    public static byte AdjustParity(\n        byte b, bool evenParity)\n    {\n        bool parity = !evenParity; // XORed with each bit to calculate parity bit\n\n        for (int i = 0; i < 7; i++)\n        {\n            bool bit = ((b >> i) & 1) == 1;\n            parity ^= bit;\n        }\n\n        if (parity)\n        {\n            return (byte)(b | 0x80);\n        }\n        else\n        {\n            return (byte)(b & 0x7F);\n        }\n    }\n}\n"
  },
  {
    "path": "kw1281test.slnx",
    "content": "<Solution>\n  <Project Path=\"kw1281test.csproj\" />\n  <Project Path=\"Tests/BitFab.KW1281Test.Tests.csproj\" />\n</Solution>\n"
  }
]