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