コード例 #1
0
        private void AddEcuMetadataToNode(TreeNode parentNode, CaesarContainer container, ECU ecu)
        {
            TreeNode rootMetadata = new TreeNode("Metadata", 26, 26);

            rootMetadata.Tag = $"RootMetadata";

            TreeNode metadataRowNode;

            metadataRowNode     = new TreeNode($"Container File Size: {container.GetFileSize()}", 9, 9);
            metadataRowNode.Tag = "RootMetadataEntry";
            rootMetadata.Nodes.Add(metadataRowNode);

            metadataRowNode     = new TreeNode($"Container Checksum: {container.FileChecksum:X8}", 9, 9);
            metadataRowNode.Tag = "RootMetadataEntry";
            rootMetadata.Nodes.Add(metadataRowNode);

            metadataRowNode     = new TreeNode($"CBF Version: {container.CaesarCFFHeader.CbfVersionString}", 9, 9);
            metadataRowNode.Tag = "RootMetadataEntry";
            rootMetadata.Nodes.Add(metadataRowNode);

            metadataRowNode     = new TreeNode($"GPD Version: {container.CaesarCFFHeader.GpdVersionString}", 9, 9);
            metadataRowNode.Tag = "RootMetadataEntry";
            rootMetadata.Nodes.Add(metadataRowNode);

            metadataRowNode     = new TreeNode($"ECU Version: {ecu.EcuXmlVersion}", 9, 9);
            metadataRowNode.Tag = "RootMetadataEntry";
            rootMetadata.Nodes.Add(metadataRowNode);

            metadataRowNode     = new TreeNode($"ECU Ignition Required: {ecu.IgnitionRequired}", 9, 9);
            metadataRowNode.Tag = "RootMetadataEntry";
            rootMetadata.Nodes.Add(metadataRowNode);


            parentNode.Nodes.Add(rootMetadata);
        }
コード例 #2
0
        private CaesarContainer PickContainer()
        {
            if (Containers.Count == 0)
            {
                MessageBox.Show("No containers have been loaded yet.");
                return(null);
            }
            CaesarContainer targetContainer = Containers[0];

            if (Containers.Count > 1)
            {
                // there isn't an embedded qualifier to identify containers easily; the ecu name is probably an easier name to identify with
                List <string[]> table = new List <string[]>();
                foreach (CaesarContainer container in Containers)
                {
                    if (container.CaesarECUs.Count > 0)
                    {
                        table.Add(new string[] { container.CaesarECUs[0].Qualifier });
                    }
                }
                GenericPicker picker = new GenericPicker(table.ToArray(), new string[] { "Container" }, 0);
                picker.Text = "Please select a container";
                if (picker.ShowDialog() != DialogResult.OK)
                {
                    return(null);
                }
                string selectedEcuQualifier = picker.SelectedResult[0];
                targetContainer = Containers.Find(x => ((x.CaesarECUs.Count > 0) && (x.CaesarECUs[0].Qualifier == selectedEcuQualifier)));
            }
            return(targetContainer);
        }
コード例 #3
0
        private void ShowAbout()
        {
            // please change this if you fork the project, thanks!
            // MessageBox.Show($"Diogenes {GetVersion()}\nCaesar {CaesarContainer.GetCaesarVersionString()}\n\nIcons from famfamfam\nhttps://github.com/jglim/CaesarSuite", "About", MessageBoxButtons.OK);
            AboutForm about = new AboutForm($"Diogenes {GetVersion()} (Caesar {CaesarContainer.GetCaesarVersionString()})");

            about.ShowDialog();
        }
コード例 #4
0
        private void loadJSONToolStripMenuItem_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();

            ofd.Title       = "Select a Caesar JSON File";
            ofd.Filter      = "JSON files (*.json)|*.json|All files (*.*)|*.*";
            ofd.Multiselect = false;
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                Containers.Add(CaesarContainer.DeserializeContainer(Encoding.UTF8.GetString(File.ReadAllBytes(ofd.FileName))));
                LoadTree();
            }
        }
コード例 #5
0
        private void loadCompressedJsonToolStripMenuItem_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();

            ofd.Title       = "Select a Compressed Caesar Binary (JSON) File";
            ofd.Filter      = "CCB files (*.ccb)|*.ccb|All files (*.*)|*.*";
            ofd.Multiselect = false;
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                Containers.Add(CaesarContainer.DeserializeCompressedContainer(File.ReadAllBytes(ofd.FileName)));
                LoadTree();
            }
        }
コード例 #6
0
 private void LoadContainers()
 {
     Containers.Clear();
     foreach (string file in Directory.GetFiles(Application.StartupPath))
     {
         if (Path.GetExtension(file).ToLower() == ".cbf")
         {
             CaesarContainer cbfContainer = new CaesarContainer(File.ReadAllBytes(file));
             Containers.Add(cbfContainer);
         }
     }
     LoadTree();
 }
コード例 #7
0
 private void TryLoadFile(string fileName)
 {
     byte[] fileBytes = File.ReadAllBytes(fileName);
     if (CaesarContainer.VerifyChecksum(fileBytes, out uint checksum))
     {
         Containers.Add(new CaesarContainer(fileBytes));
         LoadTree();
     }
     else
     {
         Console.WriteLine($"File {Path.GetFileName(fileName)} was not loaded as the checksum is invalid");
     }
 }
コード例 #8
0
        private void treeViewSelectVariantCoding(TreeNode node)
        {
            string domainName  = node.Text;
            string variantName = node.Parent.Text;
            string ecuName     = node.Parent.Parent.Text;

            Console.WriteLine($"Starting VC Dialog for {ecuName} ({variantName}) with domain as {domainName}");
            CaesarContainer container = Containers.Find(x => x.GetECUVariantByName(variantName) != null);

            // prompt the user for vc changes via VCForm
            VCForm vcForm = new VCForm(container, ecuName, variantName, domainName, Connection);

            if (vcForm.ShowDialog() == DialogResult.OK)
            {
                VariantCoding.DoVariantCoding(Connection, vcForm, allowWriteVariantCodingToolStripMenuItem.Checked);
            }
        }
コード例 #9
0
        private void FixCALs(CaesarContainer container)
        {
            int newLevel = 1;

            byte[] newFile = new byte[container.FileBytes.Length];
            Buffer.BlockCopy(container.FileBytes, 0, newFile, 0, container.FileBytes.Length);

            Console.WriteLine($"Creating a new CBF with access level requirements set at {newLevel}");
            List <DiagService> dsPendingFix = new List <DiagService>();

            using (BinaryReader reader = new BinaryReader(new MemoryStream(container.FileBytes)))
            {
                foreach (ECU ecu in container.CaesarECUs)
                {
                    foreach (DiagService ds in ecu.GlobalDiagServices)
                    {
                        if (ds.ClientAccessLevel > newLevel)
                        {
                            dsPendingFix.Add(ds);
                            Console.WriteLine($"-> {ds.Qualifier} (Level {ds.ClientAccessLevel})");
                            long fileOffset = ds.GetCALInt16Offset(reader);
                            if (fileOffset != -1)
                            {
                                newFile[fileOffset]     = (byte)newLevel;
                                newFile[fileOffset + 1] = (byte)(newLevel >> 8);
                            }
                        }
                    }
                }
                uint   checksum      = CaesarReader.ComputeFileChecksum(newFile);
                byte[] checksumBytes = BitConverter.GetBytes(checksum);
                Array.ConstrainedCopy(checksumBytes, 0, newFile, newFile.Length - 4, checksumBytes.Length);
            }

            SaveFileDialog sfd = new SaveFileDialog();

            sfd.Title  = "Specify a location to save your new CBF file";
            sfd.Filter = "CBF files (*.cbf)|*.cbf|All files (*.*)|*.*";
            if (sfd.ShowDialog() == DialogResult.OK)
            {
                File.WriteAllBytes(sfd.FileName, newFile);
            }
        }
コード例 #10
0
        private void exportContainerAsCompressedJSONToolStripMenuItem_Click(object sender, EventArgs e)
        {
            CaesarContainer targetContainer = PickContainer();

            if (targetContainer is null)
            {
                Console.WriteLine("Internal error: target container is null");
            }
            else
            {
                SaveFileDialog sfd = new SaveFileDialog();
                sfd.Title    = "Specify a location to save your new Compressed Caesar Binary (JSON) file";
                sfd.Filter   = "CCB files (*.ccb)|*.ccb|All files (*.*)|*.*";
                sfd.FileName = targetContainer.CaesarECUs[0].Qualifier;
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    File.WriteAllBytes(sfd.FileName, CaesarContainer.SerializeCompressedContainer(targetContainer));
                }
            }
        }
コード例 #11
0
        private void exportContainerAsJSONToolStripMenuItem_Click(object sender, EventArgs e)
        {
            CaesarContainer targetContainer = PickContainer();

            if (targetContainer is null)
            {
                Console.WriteLine("Internal error: target container is null");
            }
            else
            {
                SaveFileDialog sfd = new SaveFileDialog();
                sfd.Title    = "Specify a location to save your new Caesar JSON file";
                sfd.Filter   = "JSON files (*.json)|*.json|All files (*.*)|*.*";
                sfd.FileName = targetContainer.CaesarECUs[0].Qualifier;
                if (sfd.ShowDialog() == DialogResult.OK)
                {
                    File.WriteAllBytes(sfd.FileName, Encoding.UTF8.GetBytes(CaesarContainer.SerializeContainer(targetContainer)));
                }
            }
        }
コード例 #12
0
        private void listVariantIDsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            CaesarContainer targetContainer = PickContainer();

            if (targetContainer is null)
            {
                Console.WriteLine("Internal error: target container is null");
            }
            else
            {
                foreach (ECU ecu in targetContainer.CaesarECUs)
                {
                    foreach (ECUVariant variant in ecu.ECUVariants)
                    {
                        foreach (ECUVariantPattern pattern in variant.VariantPatterns)
                        {
                            Console.WriteLine($"{variant.Qualifier}: {pattern.VariantID:X4}");
                        }
                    }
                }
            }
        }
コード例 #13
0
        private void ShowAbout()
        {
            AboutForm about = new AboutForm($"Diogenes {GetVersion()} (Caesar {CaesarContainer.GetCaesarVersionString()})");

            about.ShowDialog();
        }
コード例 #14
0
 private void PostInitDebug(CaesarContainer cbfContainer)
 {
 }
コード例 #15
0
        static void Main(string[] args)
        {
#if DEBUG
            string path = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + @"\VGSNAG2.CBF";
#else
            if (args.Length == 0)
            {
                Console.WriteLine("Please run Trafo with a target CBF file as a parameter");
                return;
            }

            if (!File.Exists(args[0]))
            {
                Console.WriteLine("Specified CBF file does not exist, exiting");
                return;
            }
            string path = args[0];
#endif
            byte[] cbfBytes = File.ReadAllBytes(path);

            CaesarContainer container = new CaesarContainer(cbfBytes);

            List <object> ecuList = new List <object>();
            foreach (ECU ecu in container.CaesarECUs)
            {
                List <object> variantList = new List <object>();
                foreach (ECUVariant variant in ecu.ECUVariants)
                {
                    List <object> domainList = new List <object>();
                    foreach (VCDomain domain in variant.VCDomains)
                    {
                        List <object> fragmentList = new List <object>();
                        foreach (VCFragment fragment in domain.VCFragments)
                        {
                            var subfragmentList = new List <object>();
                            foreach (VCSubfragment subfragment in fragment.Subfragments)
                            {
                                var subfragmentRow = new { SubfragmentName = subfragment.NameCTFResolved, HexData = BitUtility.BytesToHex(subfragment.Dump) };
                                subfragmentList.Add(subfragmentRow);
                            }
                            var fragmentRow = new { FragmentName = fragment.Qualifier, BitPosition = fragment.ByteBitPos, BitSize = fragment.BitLength, Subfragments = subfragmentList };
                            fragmentList.Add(fragmentRow);
                        }
                        var domainRow = new { DomainName = domain.Qualifier, ByteSize = domain.DumpSize, ReadService = domain.ReadServiceName, WriteService = domain.WriteServiceName, VarcodingFragments = fragmentList };
                        domainList.Add(domainRow);
                    }
                    var variantRow = new { VariantName = variant.Qualifier, VarcodingDomains = domainList };
                    variantList.Add(variantRow);
                }
                var ecuRow = new { ECUName = ecu.Qualifier, ECUDescription = ecu.ECUDescriptionTranslated, ECUVariants = variantList };
                ecuList.Add(ecuRow);
            }

            var caesarContainerJson = new { TrafoVersion = GetVersion(), OriginalFile = Path.GetFileName(path), ECUs = ecuList };

            string newFilename = $"{Path.GetDirectoryName(path)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(path)}.json";

            File.WriteAllText(newFilename, JsonConvert.SerializeObject(caesarContainerJson));
            Console.WriteLine($"Converted CBF file to JSON at {newFilename}");
#if DEBUG
            Console.ReadKey();
#endif
        }
コード例 #16
0
        public static void treeViewSelectVariantCodingBackup(TreeNode node, ECUConnection connection, List <CaesarContainer> containers)
        {
            if (connection is null)
            {
                return;
            }

            Cursor.Current = Cursors.WaitCursor;

            string variantName = node.Parent.Text;
            string ecuName     = node.Parent.Parent.Text;
            string reportDate  = $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToLongTimeString()}";

            StringBuilder report = new StringBuilder();

            CaesarContainer container = containers.Find(x => x.GetECUVariantByName(variantName) != null);
            ECU             ecu       = container.GetECUByName(ecuName);
            ECUVariant      variant   = container.GetECUVariantByName(variantName);

            string containerChecksum = container.FileChecksum.ToString("X8");
            string dVersion          = MainForm.GetVersion();
            string cVersion          = CaesarContainer.GetCaesarVersionString();
            string connectionData    = connection is null ? "(Unavailable)" : connection.FriendlyProfileName;
            string ecuCbfVersion     = ecu.EcuXmlVersion;

            report.Append($"ECU Variant: {variant.Qualifier}\r\n");

            StringBuilder tableBuilder = new StringBuilder();

            // back up every domain since some have overlaps
            foreach (VCDomain domain in variant.VCDomains)
            {
                report.Append($"\r\nCoding Service: {domain.Qualifier}\r\n");
                // find the read service, then execute it as-is
                DiagService readService = variant.GetDiagServiceByName(domain.ReadServiceName);
                byte[]      response    = connection.SendDiagRequest(readService);

                // isolate the traditional vc string
                DiagPreparation largestPrep = VCForm.GetLargestPreparation(readService.OutputPreparations);
                byte[]          vcValue     = response.Skip(largestPrep.BitPosition / 8).Take(largestPrep.SizeInBits / 8).ToArray();

                StringBuilder tableRowBuilder = new StringBuilder();

                // explain the vc string's settings
                for (int i = 0; i < domain.VCFragments.Count; i++)
                {
                    VCFragment    currentFragment = domain.VCFragments[i];
                    VCSubfragment subfragment     = currentFragment.GetSubfragmentConfiguration(vcValue);

                    string fragmentValue         = subfragment is null ? "(?)" : subfragment.NameCTFResolved;
                    string fragmentSupplementKey = subfragment is null ? "(?)" : subfragment.SupplementKey;

                    string tableRowBlock = $@"
        <tr>
            <td>{currentFragment.Qualifier}</td>
            <td>{fragmentValue}</td>
            <td>{fragmentSupplementKey}</td>
        </tr>
";
                    tableRowBuilder.Append(tableRowBlock);
                }

                string tableBlock = $@"
    <hr>

    <h2>{domain.Qualifier}</h2>

    <table class=""coding-data"">
        <tr>
            <td class=""fifth"">Coding String (Hex)</td>
            <td class=""monospace"">{BitUtility.BytesToHex(vcValue, true)}</td>
        </tr>
        <tr>
            <td class=""fifth"">Raw Coding String (Hex)</td>
            <td class=""monospace"">{BitUtility.BytesToHex(response, true)}</td>
        </tr>
    </table>

    <table>
        <tr>
            <th>Fragment</th>
            <th>Value</th>
            <th>Supplement Key</th>
        </tr>
        {tableRowBuilder}
    </table>
";
                tableBuilder.Append(tableBlock);
            }


            string document = $@"
<!DOCTYPE html>
<html lang=""en"">
<head>
    <meta charset=""UTF-8"">
    <title>{ecuName} : Backup</title>
    <style>
        body
        {{
            padding: 10px 20% 15px 15%;
            font-family: sans-serif;
        }}
        .pull-right
        {{
            float: right;
        }}
        hr
        {{
            border-bottom: 0;
            opacity: 0.2;
        }}
        table
        {{
            width: 100%;
            margin: 20px 0;
        }}
        #eof
        {{
            text-transform: uppercase;
            font-weight: bold;
            opacity: 0.15;
            letter-spacing: 0.4em;
        }}
        .coding-data
        {{
            opacity: 0.8;
        }}
        .monospace
        {{
            font-family: monospace;
        }}
        .fifth
        {{
            width: 20%;
        }}
        th
        {{
            text-align: left;
        }}
    </style>
</head>
<body>
    <h1 class=""pull-right"">Diogenes</h1>
    <h1>{ecuName}</h1>
    <hr>
    <table>
        <tr>
            <td>CBF Checksum</td>
            <td>{containerChecksum}</td>
        </tr>
        <tr>
            <td>Date</td>
            <td>{reportDate}</td>
        </tr>
        <tr>
            <td>Client Version</td>
            <td>Diogenes: {dVersion}, Caesar: {cVersion}</td>
        </tr>
        <tr>
            <td>ECU CBF Version</td>
            <td>{ecuCbfVersion}</td>
        </tr>
        <tr>
            <td>ECU Variant</td>
            <td>{variantName}</td>
        </tr>
        <tr>
            <td>Connection Info</td>
            <td>{connectionData}</td>
        </tr>
    </table>

    {tableBuilder}

    <hr>

    <span id=""eof"">End of report</span>
</body>
</html>";


            Cursor.Current = Cursors.Default;

            SaveFileDialog sfd = new SaveFileDialog();

            sfd.Title    = "Specify a location to save your new VC backup";
            sfd.Filter   = "HTML file (*.html)|*.html|All files (*.*)|*.*";
            sfd.FileName = $"VC_{variantName}_{DateTime.Now.ToString("yyyyMMdd_HHmm")}.html";
            if (sfd.ShowDialog() == DialogResult.OK)
            {
                File.WriteAllText(sfd.FileName, document.ToString());
                MessageBox.Show($"Backup successfully saved to {sfd.FileName}", "Export complete");
            }
        }
コード例 #17
0
        public VCForm(CaesarContainer container, string ecuName, string variantName, string vcDomain, ECUConnection connection)
        {
            InitializeComponent();

            ECUName      = ecuName;
            VariantName  = variantName;
            VCDomainName = vcDomain;

            SelectedECU         = container.GetECUByName(ecuName);
            ECUVariant          = container.GetECUVariantByName(variantName);
            VariantCodingDomain = ECUVariant.GetVCDomainByName(VCDomainName);

            ReadService  = ECUVariant.GetDiagServiceByName(VariantCodingDomain.ReadServiceName);
            WriteService = ECUVariant.GetDiagServiceByName(VariantCodingDomain.WriteServiceName);
            if ((ReadService is null) || (WriteService is null))
            {
                Console.WriteLine("VC Dialog: Unable to proceed - could not find referenced diagnostic services");
                this.Close();
            }

            //Console.WriteLine(ReadService.Qualifier);
            //Console.WriteLine(WriteService.Qualifier);

            VCValue             = new byte[VariantCodingDomain.DumpSize];
            UnfilteredReadValue = new byte[] { };

            foreach (Tuple <string, byte[]> row in VariantCodingDomain.DefaultData)
            {
                if (row.Item1.ToLower() == "default" && (row.Item2.Length == VariantCodingDomain.DumpSize))
                {
                    VCValue = row.Item2;
                    Console.WriteLine("Default CBF variant coding data is available");
                    break;
                }
            }

            if (connection.State >= ECUConnection.ConnectionState.ChannelConnectedPendingEcuContact)
            {
                // Console.WriteLine($"Requesting variant coding read: {ReadService.Qualifier} : ({BitUtility.BytesToHex(ReadService.RequestBytes)})");
                byte[] response = connection.SendDiagRequest(ReadService);

                DiagPreparation largestPrep = GetLargestPreparation(ReadService.OutputPreparations);
                if (largestPrep.PresPoolIndex > -1)
                {
                    // DiagPresentation pres = SelectedECU.GlobalPresentations[largestPrep.PresPoolIndex];
                    // pres.PrintDebug();
                }
                // Console.WriteLine($"Variant coding received: {BitUtility.BytesToHex(response)}");

                VCValue = response.Skip(largestPrep.BitPosition / 8).Take(largestPrep.SizeInBits / 8).ToArray();
                // store the received VC: when writing back, we might need the previous author's fingerprints
                UnfilteredReadValue = response;
            }
            else
            {
                Console.WriteLine("Please check for connectivity to the target ECU (could not read variant coding data)");
                MessageBox.Show("Variant Coding dialog will operate as a simulation using default values.", "Unable to read ECU variant coding data", MessageBoxButtons.OK);
                btnApply.Enabled = false;
            }

            // VCSanityCheck();
            IntepretVC();
            PresentVC();
        }
コード例 #18
0
ファイル: DTCReport.cs プロジェクト: gibsonk1980/CaesarSuite
        public static void CreateDTCReport(List <DTCContext> dtcContexts, ECUConnection connection, ECUVariant variant)
        {
            Cursor.Current = Cursors.WaitCursor;

            string reportDate = $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToLongTimeString()}";

            string containerChecksum = variant.ParentECU.ParentContainer.FileChecksum.ToString("X8");
            string dVersion          = MainForm.GetVersion();
            string cVersion          = CaesarContainer.GetCaesarVersionString();
            string connectionData    = connection is null ? "(Unavailable)" : connection.FriendlyProfileName;
            string ecuCbfVersion     = variant.ParentECU.EcuXmlVersion;

            StringBuilder tableBuilder = new StringBuilder();

            // back up every domain since some have overlaps
            foreach (DTCContext dtcCtx in dtcContexts)
            {
                StringBuilder tableRowBuilder = new StringBuilder();

                // env, description
                for (int i = 0; i < dtcCtx.EnvironmentContext.Count; i++)
                {
                    string tableRowBlock = $@"
        <tr>
            <td>{dtcCtx.EnvironmentContext[i][0]}</td>
            <td>{dtcCtx.EnvironmentContext[i][1]}</td>
        </tr>
";
                    tableRowBuilder.Append(tableRowBlock);
                }

                string tableBlock = $@"
    <hr>

    <h2>{dtcCtx.DTC.Qualifier}</h2>

    <p>{dtcCtx.DTC.Description}</p>

    <table>
        <tr>
            <th>Environment</th>
            <th>Description</th>
        </tr>
        {tableRowBuilder}
    </table>
";
                tableBuilder.Append(tableBlock);
            }


            string document = $@"
<!DOCTYPE html>
<html lang=""en"">
<head>
    <meta charset=""UTF-8"">
    <title>{variant.ParentECU.Qualifier} : DTC Report</title>
    <style>
        body
        {{
            padding: 10px 20% 15px 15%;
            font-family: sans-serif;
        }}
        .pull-right
        {{
            float: right;
        }}
        hr
        {{
            border-bottom: 0;
            opacity: 0.2;
        }}
        table
        {{
            width: 100%;
            margin: 20px 0;
        }}
        #eof
        {{
            text-transform: uppercase;
            font-weight: bold;
            opacity: 0.15;
            letter-spacing: 0.4em;
        }}
        .coding-data
        {{
            opacity: 0.8;
        }}
        .monospace
        {{
            font-family: monospace;
        }}
        .fifth
        {{
            width: 20%;
        }}
        th
        {{
            text-align: left;
        }}
    </style>
</head>
<body>
    <h1 class=""pull-right"">Diogenes</h1>
    <h1>{variant.ParentECU.Qualifier}</h1>
    <hr>
    <table>
        <tr>
            <td>CBF Checksum</td>
            <td>{containerChecksum}</td>
        </tr>
        <tr>
            <td>Date</td>
            <td>{reportDate}</td>
        </tr>
        <tr>
            <td>Client Version</td>
            <td>Diogenes: {dVersion}, Caesar: {cVersion}</td>
        </tr>
        <tr>
            <td>ECU CBF Version</td>
            <td>{ecuCbfVersion}</td>
        </tr>
        <tr>
            <td>ECU Variant</td>
            <td>{variant.Qualifier}</td>
        </tr>
        <tr>
            <td>Connection Info</td>
            <td>{connectionData}</td>
        </tr>
    </table>

    {connection.ConnectionProtocol.QueryECUMetadata(connection).GetHtmlTable(connection)}
    {tableBuilder}

    <hr>

    <span id=""eof"">End of report</span>
</body>
</html>";


            Cursor.Current = Cursors.Default;

            SaveFileDialog sfd = new SaveFileDialog();

            sfd.Title    = "Specify a location to save your new DTC report";
            sfd.Filter   = "HTML file (*.html)|*.html|All files (*.*)|*.*";
            sfd.FileName = $"DTC_{variant.Qualifier}_{DateTime.Now.ToString("yyyyMMdd_HHmm")}.html";
            if (sfd.ShowDialog() == DialogResult.OK)
            {
                File.WriteAllText(sfd.FileName, document.ToString());
                MessageBox.Show($"Report successfully saved to {sfd.FileName}", "Export complete");
            }
        }