예제 #1
0
        /// <summary>
        /// Sends the provided message retries times, with a small delay on fail.
        /// </summary>
        /// <remarks>
        /// Returns a succsefull Response on the first successful attempt, or the failed Response if we run out of tries.
        /// </remarks>
        private async Task <Response <bool> > WritePayload(Message message, ToolPresentNotifier notifier, CancellationToken cancellationToken)
        {
            for (int i = MaxSendAttempts; i > 0; i--)
            {
                await notifier.Notify();

                if (cancellationToken.IsCancellationRequested)
                {
                    return(Response.Create(ResponseStatus.Cancelled, false));
                }

                if (!await device.SendMessage(message))
                {
                    this.logger.AddDebugMessage("WritePayload: Unable to send message.");
                    continue;
                }

                if (await this.WaitForSuccess(this.protocol.ParseUploadResponse, cancellationToken))
                {
                    return(Response.Create(ResponseStatus.Success, true));
                }

                this.logger.AddDebugMessage("WritePayload: Upload request failed.");
            }

            this.logger.AddDebugMessage("WritePayload: Giving up.");
            return(Response.Create(ResponseStatus.Error, false)); // this should be response from the loop but the compiler thinks the response variable isnt in scope here????
        }
예제 #2
0
파일: Query.cs 프로젝트: thehunt78/PcmHacks
 /// <summary>
 /// Constructor.
 /// </summary>
 public Query(Device device, Func <Message> generator, Func <Message, Response <T> > filter, ILogger logger, CancellationToken cancellationToken, ToolPresentNotifier notifier = null)
 {
     this.device            = device;
     this.generator         = generator;
     this.filter            = filter;
     this.logger            = logger;
     this.notifier          = notifier;
     this.cancellationToken = cancellationToken;
     this.MaxTimeouts       = 5;
 }
예제 #3
0
 /// <summary>
 /// Constructor.
 /// </summary>
 public Vehicle(
     Device device,
     Protocol protocol,
     ILogger logger,
     ToolPresentNotifier notifier)
 {
     this.device   = device;
     this.protocol = protocol;
     this.logger   = logger;
     this.notifier = notifier;
 }
예제 #4
0
        /// <summary>
        /// Ask all of the devices on the VPW bus for permission to switch to 4X speed.
        /// </summary>
        private async Task <List <byte> > RequestHighSpeedPermission(ToolPresentNotifier notifier)
        {
            Message permissionCheck = this.protocol.CreateHighSpeedPermissionRequest(DeviceId.Broadcast);

            await this.device.SendMessage(permissionCheck);

            // Note that as of right now, the AllPro only receives 6 of the 11 responses.
            // So until that gets fixed, we could miss a 'refuse' response and try to switch
            // to 4X anyhow. That just results in an aborted read attempt, with no harm done.
            List <byte> result     = new List <byte>();
            Message     response   = null;
            bool        anyRefused = false;

            while ((response = await this.device.ReceiveMessage()) != null)
            {
                this.logger.AddDebugMessage("Parsing " + response.GetBytes().ToHex());
                Protocol.HighSpeedPermissionResult parsed = this.protocol.ParseHighSpeedPermissionResponse(response);
                if (!parsed.IsValid)
                {
                    await Task.Delay(100);

                    continue;
                }

                result.Add(parsed.DeviceId);

                if (parsed.PermissionGranted)
                {
                    this.logger.AddUserMessage(string.Format("Module 0x{0:X2} ({1}) has agreed to enter high-speed mode.", parsed.DeviceId, DeviceId.DeviceCategory(parsed.DeviceId)));

                    // Forcing a notification message should help ELM devices receive responses.
                    await notifier.ForceNotify();

                    await Task.Delay(100);

                    continue;
                }

                this.logger.AddUserMessage(string.Format("Module 0x{0:X2} ({1}) has refused to enter high-speed mode.", parsed.DeviceId, DeviceId.DeviceCategory(parsed.DeviceId)));
                anyRefused = true;
            }

            if (anyRefused)
            {
                return(null);
            }

            return(result);
        }
예제 #5
0
        /// <summary>
        /// Copy a single memory range to the PCM.
        /// </summary>
        private async Task <bool> WriteMemoryRange(
            MemoryRange range,
            byte[] image,
            bool justTestWrite,
            ToolPresentNotifier notifier,
            CancellationToken cancellationToken)
        {
            int devicePayloadSize = device.MaxSendSize - 12; // Headers use 10 bytes, sum uses 2 bytes.

            for (int index = 0; index < range.Size; index += devicePayloadSize)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return(false);
                }

                await notifier.Notify();

                int startAddress    = (int)(range.Address + index);
                int thisPayloadSize = Math.Min(devicePayloadSize, (int)range.Size - index);

                Message payloadMessage = protocol.CreateBlockMessage(
                    image,
                    startAddress,
                    thisPayloadSize,
                    startAddress,
                    justTestWrite ? BlockCopyType.TestWrite : BlockCopyType.Copy);

                logger.AddUserMessage(
                    string.Format(
                        "Sending payload with offset 0x{0:X4}, start address 0x{1:X6}, length 0x{2:X4}.",
                        index,
                        startAddress,
                        thisPayloadSize));

                await this.WritePayload(payloadMessage, notifier, cancellationToken);

                // Not checking the success or failure here.
                // The debug pane will show if anything goes wrong, and the CRC check at the end will alert the user.
            }

            return(true);
        }
예제 #6
0
        /// <summary>
        /// Does everything required to switch to VPW 4x
        /// </summary>
        public async Task <bool> VehicleSetVPW4x(VpwSpeed newSpeed, ToolPresentNotifier notifier)
        {
            if (!device.Supports4X)
            {
                if (newSpeed == VpwSpeed.FourX)
                {
                    // where there is no support only report no switch to 4x
                    logger.AddUserMessage("This interface does not support VPW 4x");
                }
                return(true);
            }

            // Configure the vehicle bus when switching to 4x
            if (newSpeed == VpwSpeed.FourX)
            {
                logger.AddUserMessage("Attempting switch to VPW 4x");
                await device.SetTimeout(TimeoutScenario.ReadProperty);

                // The list of modules may not be useful after all, but
                // checking for an empty list indicates an uncooperative
                // module on the VPW bus.
                List <byte> modules = await this.RequestHighSpeedPermission(notifier);

                if (modules == null)
                {
                    // A device has refused the switch to high speed mode.
                    return(false);
                }

                Message broadcast = this.protocol.CreateBeginHighSpeed(DeviceId.Broadcast);
                await this.device.SendMessage(broadcast);

                // Check for any devices that refused to switch to 4X speed.
                // These responses usually get lost, so this code might be pointless.
                Message response = null;
                while ((response = await this.device.ReceiveMessage()) != null)
                {
                    Response <bool> refused = this.protocol.ParseHighSpeedRefusal(response);
                    if (refused.Status != ResponseStatus.Success)
                    {
                        // This should help ELM devices receive responses.
                        await notifier.ForceNotify();

                        continue;
                    }

                    if (refused.Value == false)
                    {
                        // TODO: Add module number.
                        this.logger.AddUserMessage("Module refused high-speed switch.");
                        return(false);
                    }
                }
            }
            else
            {
                logger.AddUserMessage("Reverting to VPW 1x");
            }

            // Request the device to change
            await device.SetVpwSpeed(newSpeed);

            TimeoutScenario scenario = newSpeed == VpwSpeed.Standard ? TimeoutScenario.ReadProperty : TimeoutScenario.ReadMemoryBlock;
            await device.SetTimeout(scenario);

            return(true);
        }
예제 #7
0
        /// <summary>
        /// Load the executable payload on the PCM at the supplied address, and execute it.
        /// </summary>
        public async Task <bool> PCMExecute(byte[] payload, int address, ToolPresentNotifier notifier, CancellationToken cancellationToken)
        {
            logger.AddUserMessage("Uploading kernel to PCM.");
            logger.AddDebugMessage("Sending upload request with payload size " + payload.Length + ", loadaddress " + address.ToString("X6"));

            // Note that we request an upload of 4k maximum, because the PCM will reject anything bigger.
            // But you can request a 4k upload and then send up to 16k if you want, and the PCM will not object.
            int claimedSize = Math.Min(4096, payload.Length);

            // Since we're going to lie about the size, we need to check for overflow ourselves.
            if (address + payload.Length > 0xFFCDFF)
            {
                logger.AddUserMessage("Base address and size would exceed usable RAM.");
                return(false);
            }

            Message request = protocol.CreateUploadRequest(address, claimedSize);

            if (!await TrySendMessage(request, "upload request"))
            {
                return(false);
            }

            if (!await this.WaitForSuccess(this.protocol.ParseUploadPermissionResponse, cancellationToken))
            {
                logger.AddUserMessage("Permission to upload kernel was denied.");
                return(false);
            }

            if (cancellationToken.IsCancellationRequested)
            {
                return(false);
            }

            logger.AddDebugMessage("Going to load a " + payload.Length + " byte payload to 0x" + address.ToString("X6"));

            await this.device.SetTimeout(TimeoutScenario.SendKernel);

            // Loop through the payload building and sending packets, highest first, execute on last
            int payloadSize = device.MaxSendSize - 12; // Headers use 10 bytes, sum uses 2 bytes.
            int chunkCount  = payload.Length / payloadSize;
            int remainder   = payload.Length % payloadSize;

            int offset       = (chunkCount * payloadSize);
            int startAddress = address + offset;

            // First we send the 'remainder' payload, containing any bytes that won't fill up an entire upload packet.
            logger.AddDebugMessage(
                string.Format(
                    "Sending remainder payload with offset 0x{0:X}, start address 0x{1:X}, length 0x{2:X}.",
                    offset,
                    startAddress,
                    remainder));

            Message remainderMessage = protocol.CreateBlockMessage(
                payload,
                offset,
                remainder,
                address + offset,
                remainder == payload.Length ? BlockCopyType.Execute : BlockCopyType.Copy);

            await notifier.Notify();

            Response <bool> uploadResponse = await WritePayload(remainderMessage, notifier, cancellationToken);

            if (uploadResponse.Status != ResponseStatus.Success)
            {
                logger.AddDebugMessage("Could not upload kernel to PCM, remainder payload not accepted.");
                return(false);
            }

            // Now we send a series of full upload packets
            for (int chunkIndex = chunkCount; chunkIndex > 0; chunkIndex--)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return(false);
                }

                offset       = (chunkIndex - 1) * payloadSize;
                startAddress = address + offset;
                Message payloadMessage = protocol.CreateBlockMessage(
                    payload,
                    offset,
                    payloadSize,
                    startAddress,
                    offset == 0 ? BlockCopyType.Execute : BlockCopyType.Copy);

                logger.AddDebugMessage(
                    string.Format(
                        "Sending payload with offset 0x{0:X6}, start address 0x{1:X6}, length 0x{2:X4}.",
                        offset,
                        startAddress,
                        payloadSize));

                uploadResponse = await WritePayload(payloadMessage, notifier, cancellationToken);

                if (uploadResponse.Status != ResponseStatus.Success)
                {
                    logger.AddDebugMessage("Could not upload kernel to PCM, payload not accepted.");
                    return(false);
                }

                int bytesSent   = payload.Length - offset;
                int percentDone = bytesSent * 100 / payload.Length;

                this.logger.AddUserMessage(
                    string.Format(
                        "Kernel upload {0}% complete.",
                        percentDone));
            }

            // Consider: return kernel version rather than boolean?
            UInt32 kernelVersion = await this.GetKernelVersion();

            this.logger.AddUserMessage("Kernel Version: " + kernelVersion.ToString("X8"));

            return(true);
        }
예제 #8
0
        /// <summary>
        /// Read the full contents of the PCM.
        /// Assumes the PCM is unlocked an were ready to go
        /// </summary>
        public async Task<Response<Stream>> ReadContents(PcmInfo info, CancellationToken cancellationToken)
        {
            try
            {
                this.device.ClearMessageQueue();

                // This must precede the switch to 4X.
                ToolPresentNotifier toolPresentNotifier = new ToolPresentNotifier(this.logger, this.messageFactory, this.device);
                await toolPresentNotifier.Notify();

                // switch to 4x, if possible. But continue either way.
                // if the vehicle bus switches but the device does not, the bus will need to time out to revert back to 1x, and the next steps will fail.
                if (!await this.VehicleSetVPW4x(VpwSpeed.FourX))
                {
                    this.logger.AddUserMessage("Stopping here because we were unable to switch to 4X.");
                    return Response.Create(ResponseStatus.Error, (Stream)null);
                }

                await toolPresentNotifier.Notify();

                // execute read kernel
                Response<byte[]> response = await LoadKernelFromFile("kernel.bin");
                if (response.Status != ResponseStatus.Success)
                {
                    logger.AddUserMessage("Failed to load kernel from file.");
                    return new Response<Stream>(response.Status, null);
                }
                
                if (cancellationToken.IsCancellationRequested)
                {
                    return Response.Create(ResponseStatus.Cancelled, (Stream)null);
                }

                await toolPresentNotifier.Notify();

                // TODO: instead of this hard-coded 0xFF9150, get the base address from the PcmInfo object.
                if (!await PCMExecute(response.Value, 0xFF9150, cancellationToken))
                {
                    logger.AddUserMessage("Failed to upload kernel to PCM");

                    return new Response<Stream>(
                        cancellationToken.IsCancellationRequested ? ResponseStatus.Cancelled : ResponseStatus.Error, 
                        null);
                }

                logger.AddUserMessage("kernel uploaded to PCM succesfully. Requesting data...");

                await this.device.SetTimeout(TimeoutScenario.ReadMemoryBlock);

                int startAddress = info.ImageBaseAddress;
                int endAddress = info.ImageBaseAddress + info.ImageSize;
                int bytesRemaining = info.ImageSize;
                int blockSize = this.device.MaxReceiveSize - 10 - 2; // allow space for the header and block checksum

                byte[] image = new byte[info.ImageSize];

                while (startAddress < endAddress)
                {
                    if (cancellationToken.IsCancellationRequested)
                    {
                        return Response.Create(ResponseStatus.Cancelled, (Stream)null);
                    }

                    await toolPresentNotifier.Notify();

                    if (startAddress + blockSize > endAddress)
                    {
                        blockSize = endAddress - startAddress;
                    }

                    if (blockSize < 1)
                    {
                        this.logger.AddUserMessage("Image download complete");
                        break;
                    }
                    
                    if (!await TryReadBlock(image, blockSize, startAddress))
                    {
                        this.logger.AddUserMessage(
                            string.Format(
                                "Unable to read block from {0} to {1}",
                                startAddress,
                                (startAddress + blockSize) - 1));
                        return new Response<Stream>(ResponseStatus.Error, null);
                    }

                    startAddress += blockSize;
                }

                await this.Cleanup(); // Not sure why this does not get called in the finally block on successfull read?

                MemoryStream stream = new MemoryStream(image);
                return new Response<Stream>(ResponseStatus.Success, stream);
            }
            catch(Exception exception)
            {
                this.logger.AddUserMessage("Something went wrong. " + exception.Message);
                this.logger.AddDebugMessage(exception.ToString());
                return new Response<Stream>(ResponseStatus.Error, null);
            }
            finally
            {
                // Sending the exit command at both speeds and revert to 1x.
                await this.Cleanup();
            }
        }
예제 #9
0
        /// <summary>
        /// Compare CRCs from the file to CRCs from the PCM.
        /// </summary>
        private async Task <bool> CompareRanges(
            IList <MemoryRange> ranges,
            byte[] image,
            BlockType blockTypes,
            ToolPresentNotifier notifier,
            CancellationToken cancellationToken)
        {
            logger.AddUserMessage("Requesting CRCs from PCM...");
            await notifier.Notify();

            // The kernel will remember (and return) the CRC value of the last block it
            // was asked about, which leads to misleading results if you only rewrite
            // a single block. So we send a a bogus query to reset the last-used CRC
            // value in the kernel.
            Query <UInt32> crcReset = new Query <uint>(
                this.device,
                () => this.protocol.CreateCrcQuery(0, 0),
                (message) => this.protocol.ParseCrc(message, 0, 0),
                this.logger,
                cancellationToken,
                notifier);
            await crcReset.Execute();

            await this.device.SetTimeout(TimeoutScenario.ReadCrc);

            bool successForAllRanges = true;

            foreach (MemoryRange range in ranges)
            {
                if ((range.Type & blockTypes) == 0)
                {
                    this.logger.AddUserMessage(
                        string.Format(
                            "Range {0:X6}-{1:X6} - Not needed for this operation.",
                            range.Address,
                            range.Address + (range.Size - 1)));
                    continue;
                }

                await notifier.Notify();

                this.device.ClearMessageQueue();
                bool   success = false;
                UInt32 crc     = 0;

                // You might think that a shorter retry delay would speed things up,
                // but 1500ms delay gets CRC results in about 3.5 seconds.
                // A 1000ms delay resulted in 4+ second CRC responses, and a 750ms
                // delay resulted in 5 second CRC responses. The PCM needs to spend
                // its time caculating CRCs rather than responding to messages.
                int     retryDelay = 1500;
                Message query      = this.protocol.CreateCrcQuery(range.Address, range.Size);
                for (int attempts = 0; attempts < 10; attempts++)
                {
                    if (cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }

                    await notifier.Notify();

                    if (!await this.device.SendMessage(query))
                    {
                        // This delay is fast because we're waiting for the bus to be available,
                        // rather than waiting for the PCM's CPU to finish computing the CRC as
                        // with the other two delays below.
                        await Task.Delay(100);

                        continue;
                    }

                    Message response = await this.device.ReceiveMessage();

                    if (response == null)
                    {
                        await Task.Delay(retryDelay);

                        continue;
                    }

                    Response <UInt32> crcResponse = this.protocol.ParseCrc(response, range.Address, range.Size);
                    if (crcResponse.Status != ResponseStatus.Success)
                    {
                        await Task.Delay(retryDelay);

                        continue;
                    }

                    success = true;
                    crc     = crcResponse.Value;
                    break;
                }

                this.device.ClearMessageQueue();

                if (!success)
                {
                    this.logger.AddUserMessage("Unable to get CRC for memory range " + range.Address.ToString("X8") + " / " + range.Size.ToString("X8"));
                    successForAllRanges = false;
                    continue;
                }

                range.ActualCrc = crc;

                this.logger.AddUserMessage(
                    string.Format(
                        "Range {0:X6}-{1:X6} - File: {2:X8} - PCM: {3:X8} - ({4}) - {5}",
                        range.Address,
                        range.Address + (range.Size - 1),
                        range.DesiredCrc,
                        range.ActualCrc,
                        range.Type,
                        range.DesiredCrc == range.ActualCrc ? "Same" : "Different"));
            }

            await notifier.Notify();

            foreach (MemoryRange range in ranges)
            {
                if ((range.Type & blockTypes) == 0)
                {
                    continue;
                }

                if (range.ActualCrc != range.DesiredCrc)
                {
                    return(false);
                }
            }

            this.device.ClearMessageQueue();

            return(successForAllRanges);
        }
예제 #10
0
        /// <summary>
        /// Write the calibration blocks.
        /// </summary>
        private async Task <bool> Write(CancellationToken cancellationToken, byte[] image, WriteType writeType, ToolPresentNotifier notifier)
        {
            await notifier.Notify();

            BlockType relevantBlocks;

            switch (writeType)
            {
            case WriteType.Compare:
                relevantBlocks = BlockType.All;
                break;

            case WriteType.TestWrite:
                relevantBlocks = BlockType.Calibration;
                break;

            case WriteType.Calibration:
                relevantBlocks = BlockType.Calibration;
                break;

            case WriteType.Parameters:
                relevantBlocks = BlockType.Parameter;
                break;

            case WriteType.OsAndCalibration:
                relevantBlocks = BlockType.Calibration | BlockType.OperatingSystem;
                break;

            case WriteType.Full:
                relevantBlocks = BlockType.All;
                break;

            default:
                throw new InvalidDataException("Unsuppported operation type: " + writeType.ToString());
            }

            // Which flash chip?
            await notifier.Notify();

            await this.device.SetTimeout(TimeoutScenario.ReadProperty);

            Query <UInt32> chipIdQuery = new Query <uint>(
                this.device,
                this.protocol.CreateFlashMemoryTypeQuery,
                this.protocol.ParseFlashMemoryType,
                this.logger,
                cancellationToken,
                notifier);
            Response <UInt32> chipIdResponse = await chipIdQuery.Execute();

            if (chipIdResponse.Status != ResponseStatus.Success)
            {
                logger.AddUserMessage("Unable to determine which flash chip is in this PCM");
                return(false);
            }

            logger.AddUserMessage("Flash memory type: " + chipIdResponse.Value.ToString("X8"));

            // known chips
            // http://ftp1.digi.com/support/documentation/jtag_v410_flashes.pdf
            string Amd   = "Amd";               // 0001
            string Intel = "Intel";             // 0089
            string I4471 = "28F400B5-B 512Kb";  // 4471
            string A2258 = "Am29F800B 1Mbyte";  // 2258
            string I889D = "28F800B5-B 1Mbyte"; // 889D

            logger.AddUserMessage("Flash memory ID: " + chipIdResponse.Value.ToString("X8"));
            if ((chipIdResponse.Value >> 16) == 0x0001)
            {
                logger.AddUserMessage("Flash memory manufactuer: " + Amd);
            }
            if ((chipIdResponse.Value >> 16) == 0x0089)
            {
                logger.AddUserMessage("Flash memory manufactuer: " + Intel);
            }
            if ((chipIdResponse.Value & 0xFFFF) == 0x4471)
            {
                logger.AddUserMessage("Flash memory type: " + I4471);
            }
            if ((chipIdResponse.Value & 0xFFFF) == 0x2258)
            {
                logger.AddUserMessage("Flash memory type: " + A2258);
            }
            if ((chipIdResponse.Value & 0xFFFF) == 0x889D)
            {
                logger.AddUserMessage("Flash memory type: " + I889D);
            }

            await notifier.Notify();

            IList <MemoryRange> ranges = this.GetMemoryRanges(chipIdResponse.Value);

            if (ranges == null)
            {
                return(false);
            }

            logger.AddUserMessage("Computing CRCs from local file...");
            this.GetCrcFromImage(ranges, image);

            if (await this.CompareRanges(
                    ranges,
                    image,
                    relevantBlocks,
                    notifier,
                    cancellationToken))
            {
                // Don't stop here if the user just wants to test their cable.
                if (writeType != WriteType.TestWrite)
                {
                    this.logger.AddUserMessage("All ranges are identical.");
                    return(true);
                }
            }

            // Stop now if the user only requested a comparison.
            if (writeType == WriteType.Compare)
            {
                this.logger.AddUserMessage("Note that mismatched Parameter blocks are to be expected.");
                this.logger.AddUserMessage("Parameter data can change every time the PCM is used.");
                return(true);
            }

            // Erase and rewrite the required memory ranges.
            await this.device.SetTimeout(TimeoutScenario.Maximum);

            foreach (MemoryRange range in ranges)
            {
                // We'll send a tool-present message during the erase request.
                if ((range.ActualCrc == range.DesiredCrc) && (writeType != WriteType.TestWrite))
                {
                    continue;
                }

                if ((range.Type & relevantBlocks) == 0)
                {
                    continue;
                }

                this.logger.AddUserMessage(
                    string.Format(
                        "Processing range {0:X6}-{1:X6}",
                        range.Address,
                        range.Address + (range.Size - 1)));

                if (writeType != WriteType.TestWrite)
                {
                    this.logger.AddUserMessage("Erasing");

                    Query <byte> eraseRequest = new Query <byte>(
                        this.device,
                        () => this.protocol.CreateFlashEraseBlockRequest(range.Address),
                        this.protocol.ParseFlashEraseBlock,
                        this.logger,
                        cancellationToken,
                        notifier);

                    eraseRequest.MaxTimeouts = 5; // Reduce this when we know how many are likely to be needed.
                    Response <byte> eraseResponse = await eraseRequest.Execute();

                    if (eraseResponse.Status != ResponseStatus.Success)
                    {
                        this.logger.AddUserMessage("Unable to erase flash memory: " + eraseResponse.Status.ToString());
                        this.RequestDebugLogs(cancellationToken);
                        return(false);
                    }

                    if (eraseResponse.Value != 0x80)
                    {
                        this.logger.AddUserMessage("Unable to erase flash memory. Code: " + eraseResponse.Value.ToString("X2"));
                        this.RequestDebugLogs(cancellationToken);
                        return(false);
                    }
                }

                if (writeType == WriteType.TestWrite)
                {
                    this.logger.AddUserMessage("Testing...");
                }
                else
                {
                    this.logger.AddUserMessage("Writing...");
                }

                await notifier.Notify();

                await WriteMemoryRange(
                    range,
                    image,
                    writeType == WriteType.TestWrite,
                    notifier,
                    cancellationToken);
            }

            bool match = await this.CompareRanges(ranges, image, relevantBlocks, notifier, cancellationToken);

            if (writeType == WriteType.TestWrite)
            {
                // TODO: the app should know if any errors were encountered. The user shouldn't need to check.
                this.logger.AddUserMessage("Test complete. Were any errors logged above?");
                return(true);
            }

            if (match)
            {
                this.logger.AddUserMessage("Flash successful!");
                return(true);
            }

            this.logger.AddUserMessage("===============================================");
            this.logger.AddUserMessage("THE CHANGES WERE -NOT- WRITTEN SUCCESSFULLY");
            this.logger.AddUserMessage("===============================================");

            if (cancellationToken.IsCancellationRequested)
            {
                this.logger.AddUserMessage("");
                this.logger.AddUserMessage("The operation was cancelled.");
                this.logger.AddUserMessage("This PCM is probably not usable in its current state.");
                this.logger.AddUserMessage("");
            }
            else
            {
                this.logger.AddUserMessage("Don't panic. Also, don't try to drive this car.");
                this.logger.AddUserMessage("Please try flashing again. Preferably now.");
                this.logger.AddUserMessage("In most cases, the second try will succeed.");
                this.logger.AddUserMessage("");
                this.logger.AddUserMessage("If this happens three times in a row, please");
                this.logger.AddUserMessage("start a new thread at pcmhacking.net, and");
                this.logger.AddUserMessage("include the contents of the debug tab.");
                this.logger.AddUserMessage("");
                this.RequestDebugLogs(cancellationToken);
            }

            return(false);
        }