// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : Pregap.cs // Author(s) : Natalia Portillo // // Component : CompactDisc dumping. // // --[ Description ] ---------------------------------------------------------- // // Calculates CompactDisc track pregaps. // // --[ License ] -------------------------------------------------------------- // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2021 Natalia Portillo // ****************************************************************************/ using System; using System.Collections.Generic; using System.IO; using System.Linq; using Aaru.Checksums; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Structs; using Aaru.Console; using Aaru.Core.Logging; using Aaru.Devices; // ReSharper disable JoinDeclarationAndInitializer // ReSharper disable InlineOutVariableDeclaration // ReSharper disable TooWideLocalVariableScope namespace Aaru.Core.Devices.Dumping { partial class Dump { // TODO: Fix offset /// Reads the first track pregap from a CompactDisc /// Block size in bytes /// Current speed /// List of media tags /// Subchannel the drive can read /// Total time spent sending commands to a drive void ReadCdFirstTrackPregap(uint blockSize, ref double currentSpeed, Dictionary mediaTags, MmcSubchannel supportedSubchannel, ref double totalDuration) { bool sense; // Sense indicator byte[] cmdBuf; // Data buffer double cmdDuration; // Command execution time DateTime timeSpeedStart; // Time of start for speed calculation ulong sectorSpeedStart = 0; // Used to calculate correct speed bool gotFirstTrackPregap = false; int firstTrackPregapSectorsGood = 0; var firstTrackPregapMs = new MemoryStream(); _dumpLog.WriteLine("Reading first track pregap"); UpdateStatus?.Invoke("Reading first track pregap"); InitProgress?.Invoke(); timeSpeedStart = DateTime.UtcNow; for(int firstTrackPregapBlock = -150; firstTrackPregapBlock < 0 && _resume.NextBlock == 0; firstTrackPregapBlock++) { if(_aborted) { _dumpLog.WriteLine("Aborted!"); UpdateStatus?.Invoke("Aborted!"); break; } PulseProgress?. Invoke($"Trying to read first track pregap sector {firstTrackPregapBlock} ({currentSpeed:F3} MiB/sec.)"); // ReSharper disable IntVariableOverflowInUncheckedContext sense = _dev.ReadCd(out cmdBuf, out _, (uint)firstTrackPregapBlock, blockSize, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, supportedSubchannel, _dev.Timeout, out cmdDuration); // ReSharper restore IntVariableOverflowInUncheckedContext if(!sense && !_dev.Error) { firstTrackPregapMs.Write(cmdBuf, 0, (int)blockSize); gotFirstTrackPregap = true; firstTrackPregapSectorsGood++; totalDuration += cmdDuration; } else { // Write empty data if(gotFirstTrackPregap) firstTrackPregapMs.Write(new byte[blockSize], 0, (int)blockSize); } sectorSpeedStart++; double elapsed = (DateTime.UtcNow - timeSpeedStart).TotalSeconds; if(elapsed <= 0) continue; currentSpeed = sectorSpeedStart * blockSize / (1048576 * elapsed); sectorSpeedStart = 0; timeSpeedStart = DateTime.UtcNow; } if(firstTrackPregapSectorsGood > 0) mediaTags.Add(MediaTagType.CD_FirstTrackPregap, firstTrackPregapMs.ToArray()); EndProgress?.Invoke(); UpdateStatus?.Invoke($"Got {firstTrackPregapSectorsGood} first track pregap sectors."); _dumpLog.WriteLine("Got {0} first track pregap sectors.", firstTrackPregapSectorsGood); firstTrackPregapMs.Close(); } /// Calculate track pregaps /// Device /// Dumping log /// Progress update callback /// List of tracks /// Set if drive supports reading PQ subchannel /// Set if drive supports reading RW subchannel /// Database entry for device /// Set if we found the drive does not return the exact subchannel we requested /// Set if dumping, otherwise media info public static void SolveTrackPregaps(Device dev, DumpLog dumpLog, UpdateStatusHandler updateStatus, Track[] tracks, bool supportsPqSubchannel, bool supportsRwSubchannel, Database.Models.Device dbDev, out bool inexactPositioning, bool dumping) { bool sense = true; // Sense indicator byte[] subBuf = null; int posQ; uint retries; bool? bcd = null; byte[] crc; Dictionary pregaps = new Dictionary(); inexactPositioning = false; if(!supportsPqSubchannel && !supportsRwSubchannel) return; // Check if subchannel is BCD for(retries = 0; retries < 10; retries++) { sense = supportsRwSubchannel ? GetSectorForPregapRaw(dev, 11, dbDev, out subBuf, false) : GetSectorForPregapQ16(dev, 11, out subBuf, false); if(sense) continue; bcd = (subBuf[9] & 0x10) > 0; break; } AaruConsole.DebugWriteLine("Pregap calculator", bcd == true ? "Subchannel is BCD" : bcd == false ? "Subchannel is not BCD" : "Could not detect drive subchannel BCD"); if(bcd is null) { dumpLog?.WriteLine("Could not detect if drive subchannel is BCD or not, pregaps could not be calculated, dump may be incorrect..."); updateStatus?. Invoke("Could not detect if drive subchannel is BCD or not, pregaps could not be calculated, dump may be incorrect..."); return; } // Initialize the dictionary foreach(Track t in tracks) pregaps[t.Sequence] = 0; for(int t = 0; t < tracks.Length; t++) { Track track = tracks[t]; int trackRetries = 0; // First track of each session has at least 150 sectors of pregap and is not always readable if(tracks.Where(trk => trk.Session == track.Session).OrderBy(trk => trk.Sequence). FirstOrDefault().Sequence == track.Sequence) { AaruConsole.DebugWriteLine("Pregap calculator", "Skipping track {0}", track.Sequence); if(track.Sequence > 1) pregaps[track.Sequence] = 150; continue; } if(t > 0 && tracks[t - 1].Type == tracks[t].Type && dumping) { AaruConsole.DebugWriteLine("Pregap calculator", "Skipping track {0}", track.Sequence); continue; } if(dumping && dev.Manufacturer.ToLowerInvariant().StartsWith("plextor", StringComparison.Ordinal)) { AaruConsole.DebugWriteLine("Pregap calculator", "Skipping track {0} due to Plextor firmware bug", track.Sequence); continue; } AaruConsole.DebugWriteLine("Pregap calculator", "Track {0}", track.Sequence); int lba = (int)track.StartSector - 1; bool pregapFound = false; Track previousTrack = tracks.FirstOrDefault(trk => trk.Sequence == track.Sequence - 1); bool goneBack = false; bool goFront = false; bool forward = false; bool crcOk = false; bool previousPregapIsPreviousTrack = false; // Check if pregap is 0 for(retries = 0; retries < 10 && !pregapFound; retries++) { sense = supportsRwSubchannel ? GetSectorForPregapRaw(dev, (uint)lba, dbDev, out subBuf, track.Type == TrackType.Audio) : GetSectorForPregapQ16(dev, (uint)lba, out subBuf, track.Type == TrackType.Audio); if(sense) { AaruConsole.DebugWriteLine("Pregap calculator", "LBA: {0}, Try {1}, Sense {2}", lba, retries + 1, sense); continue; } if(bcd == false) BinaryToBcdQ(subBuf); CRC16CCITTContext.Data(subBuf, 10, out crc); AaruConsole.DebugWriteLine("Pregap calculator", "LBA: {0}, Try {1}, Sense {2}, Q: {3:X2} {4:X2} {5:X2} {6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} {12:X2} CRC 0x{13:X2}{14:X2}, Calculated CRC: 0x{15:X2}{16:X2}", lba, retries + 1, sense, subBuf[0], subBuf[1], subBuf[2], subBuf[3], subBuf[4], subBuf[5], subBuf[6], subBuf[7], subBuf[8], subBuf[9], subBuf[10], subBuf[11], crc[0], crc[1]); crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11]; // Try to do a simple correction if(!crcOk) { // Data track cannot have 11xxb in CONTROL if((subBuf[0] & 0x40) > 0) subBuf[0] &= 0x7F; // ADR only uses two bits subBuf[0] &= 0xF3; // Don't care about other Q modes if((subBuf[0] & 0xF) == 1) { // ZERO only used in DDCD subBuf[6] = 0; // Fix BCD numbering for(int i = 1; i < 10; i++) { if((subBuf[i] & 0xF0) > 0xA0) subBuf[i] &= 0x7F; if((subBuf[i] & 0x0F) > 0x0A) subBuf[i] &= 0xF7; } } CRC16CCITTContext.Data(subBuf, 10, out crc); crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11]; if(crcOk) { AaruConsole.DebugWriteLine("Pregap calculator", "LBA: {0}, Try {1}, Sense {2}, Q (FIXED): {3:X2} {4:X2} {5:X2} {6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} {12:X2} CRC 0x{13:X2}{14:X2}, Calculated CRC: 0x{15:X2}{16:X2}", lba, retries + 1, sense, subBuf[0], subBuf[1], subBuf[2], subBuf[3], subBuf[4], subBuf[5], subBuf[6], subBuf[7], subBuf[8], subBuf[9], subBuf[10], subBuf[11], crc[0], crc[1]); } else continue; } BcdToBinaryQ(subBuf); // Q position if((subBuf[0] & 0xF) != 1) continue; posQ = (subBuf[7] * 60 * 75) + (subBuf[8] * 75) + subBuf[9] - 150; if(subBuf[1] != track.Sequence - 1 || subBuf[2] == 0 || posQ != lba) break; pregaps[track.Sequence] = 0; pregapFound = true; } if(pregapFound) continue; // Calculate pregap lba = (int)track.StartSector - 150; while(lba > (int)previousTrack.StartSector && lba <= (int)track.StartSector) { // Some drives crash if you try to read just before the previous read, so seek away first if(!forward) sense = supportsRwSubchannel ? GetSectorForPregapRaw(dev, (uint)lba - 10, dbDev, out subBuf, track.Type == TrackType.Audio) : GetSectorForPregapQ16(dev, (uint)lba - 10, out subBuf, track.Type == TrackType.Audio); for(retries = 0; retries < 10; retries++) { sense = supportsRwSubchannel ? GetSectorForPregapRaw(dev, (uint)lba, dbDev, out subBuf, track.Type == TrackType.Audio) : GetSectorForPregapQ16(dev, (uint)lba, out subBuf, track.Type == TrackType.Audio); if(sense) continue; if(bcd == false) BinaryToBcdQ(subBuf); CRC16CCITTContext.Data(subBuf, 10, out crc); AaruConsole.DebugWriteLine("Pregap calculator", "LBA: {0}, Try {1}, Sense {2}, Q: {3:X2} {4:X2} {5:X2} {6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} {12:X2} CRC 0x{13:X2}{14:X2}, Calculated CRC: 0x{15:X2}{16:X2}", lba, retries + 1, sense, subBuf[0], subBuf[1], subBuf[2], subBuf[3], subBuf[4], subBuf[5], subBuf[6], subBuf[7], subBuf[8], subBuf[9], subBuf[10], subBuf[11], crc[0], crc[1]); crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11]; // Try to do a simple correction if(!crcOk) { // Data track cannot have 11xxb in CONTROL if((subBuf[0] & 0x40) > 0) subBuf[0] &= 0x7F; // ADR only uses two bits subBuf[0] &= 0xF3; // Don't care about other Q modes if((subBuf[0] & 0xF) == 1) { // ZERO only used in DDCD subBuf[6] = 0; // Fix BCD numbering for(int i = 1; i < 10; i++) { if((subBuf[i] & 0xF0) > 0xA0) subBuf[i] &= 0x7F; if((subBuf[i] & 0x0F) > 0x0A) subBuf[i] &= 0xF7; } } CRC16CCITTContext.Data(subBuf, 10, out crc); crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11]; if(crcOk) { AaruConsole.DebugWriteLine("Pregap calculator", "LBA: {0}, Try {1}, Sense {2}, Q (FIXED): {3:X2} {4:X2} {5:X2} {6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} {12:X2} CRC 0x{13:X2}{14:X2}, Calculated CRC: 0x{15:X2}{16:X2}", lba, retries + 1, sense, subBuf[0], subBuf[1], subBuf[2], subBuf[3], subBuf[4], subBuf[5], subBuf[6], subBuf[7], subBuf[8], subBuf[9], subBuf[10], subBuf[11], crc[0], crc[1]); break; } } if(crcOk) break; } if(retries == 10) { if(sense) { trackRetries++; if(trackRetries >= 10) { if(pregaps[track.Sequence] == 0) { if((previousTrack.Type == TrackType.Audio && track.Type != TrackType.Audio) || (previousTrack.Type != TrackType.Audio && track.Type == TrackType.Audio)) { dumpLog?. WriteLine("Could not read subchannel for this track, supposing 150 sectors."); updateStatus?. Invoke("Could not read subchannel for this track, supposing 150 sectors."); } else { dumpLog?. WriteLine("Could not read subchannel for this track, supposing 0 sectors."); updateStatus?. Invoke("Could not read subchannel for this track, supposing 0 sectors."); } } else { dumpLog?. WriteLine($"Could not read subchannel for this track, supposing {pregaps[track.Sequence]} sectors."); updateStatus?. Invoke($"Could not read subchannel for this track, supposing {pregaps[track.Sequence]} sectors."); } break; } dumpLog?.WriteLine($"Could not read subchannel for sector {lba}"); updateStatus?.Invoke($"Could not read subchannel for sector {lba}"); lba++; forward = true; continue; } dumpLog?.WriteLine($"Could not get correct subchannel for sector {lba}"); updateStatus?.Invoke($"Could not get correct subchannel for sector {lba}"); } if(subBuf.All(b => b == 0)) { inexactPositioning = true; AaruConsole.DebugWriteLine("Pregap calculator", "All Q empty for LBA {0}", lba); break; } BcdToBinaryQ(subBuf); // If it's not Q position if((subBuf[0] & 0xF) != 1) { // This means we already searched back, so search forward if(goFront) { lba++; forward = true; if(lba == (int)previousTrack.StartSector) break; continue; } // Search back goneBack = true; lba--; forward = false; continue; } // Previous track if(subBuf[1] < track.Sequence) { lba++; forward = true; previousPregapIsPreviousTrack = true; // Already gone back, so go forward if(goneBack) goFront = true; continue; } // Same track, but not pregap if(subBuf[1] == track.Sequence && subBuf[2] > 0) { lba--; forward = false; if(previousPregapIsPreviousTrack) break; continue; } previousPregapIsPreviousTrack = false; // Pregap according to Q position posQ = (subBuf[7] * 60 * 75) + (subBuf[8] * 75) + subBuf[9] - 150; int diff = posQ - lba; int pregapQ = (int)track.StartSector - lba; if(diff != 0) { AaruConsole.DebugWriteLine("Pregap calculator", "Invalid Q position for LBA {0}, got {1}", lba, posQ); inexactPositioning = true; } // Received a Q post the LBA we wanted, just go back. If we are already going forward, break if(posQ > lba) { if(forward) break; lba--; continue; } // Bigger than known change, otherwise we found it if(pregapQ > pregaps[track.Sequence]) { // If CRC is not OK, only accept pregaps less than 10 sectors longer than previously now if(crcOk || pregapQ - pregaps[track.Sequence] < 10) { AaruConsole.DebugWriteLine("Pregap calculator", "Pregap for track {0}: {1}", track.Sequence, pregapQ); pregaps[track.Sequence] = pregapQ; } // We are going forward, so we have already been in the previous track, so add 1 to pregap and get out of here else if(forward) { pregaps[track.Sequence]++; break; } } else if(pregapQ == pregaps[track.Sequence]) break; lba--; forward = false; } } foreach(Track trk in tracks) { trk.Pregap = (ulong)pregaps[trk.Sequence]; // Do not reduce pregap, or starting position of session's first track if(tracks.Where(t => t.Session == trk.Session).OrderBy(t => t.Sequence).FirstOrDefault(). Sequence == trk.Sequence) continue; if(dumping) { // Minus five, to ensure dumping will fix if there is a pregap LBA 0 int red = 5; while(trk.Pregap > 0 && red > 0) { trk.Pregap--; red--; } } trk.StartSector -= trk.Pregap; #if DEBUG dumpLog?.WriteLine($"Track {trk.Sequence} pregap is {trk.Pregap} sectors"); updateStatus?.Invoke($"Track {trk.Sequence} pregap is {trk.Pregap} sectors"); #endif } } /// Reads a RAW subchannel sector for pregap calculation /// Device /// LBA /// Database entry for device /// Read subchannel /// Set if it is an audio track /// true if read correctly, false otherwise static bool GetSectorForPregapRaw(Device dev, uint lba, Database.Models.Device dbDev, out byte[] subBuf, bool audioTrack) { byte[] cmdBuf; bool sense; subBuf = null; if(audioTrack) { sense = dev.ReadCd(out cmdBuf, out _, lba, 2448, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); if(sense) sense = dev.ReadCd(out cmdBuf, out _, lba, 2448, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); } else { sense = dev.ReadCd(out cmdBuf, out _, lba, 2448, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); if(sense) sense = dev.ReadCd(out cmdBuf, out _, lba, 2448, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); } if(!sense) { byte[] tmpBuf = new byte[96]; Array.Copy(cmdBuf, 2352, tmpBuf, 0, 96); subBuf = DeinterleaveQ(tmpBuf); } else { sense = dev.ReadCd(out cmdBuf, out _, lba, 96, 1, MmcSectorTypes.AllTypes, false, false, false, MmcHeaderCodes.None, false, false, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); if(sense) sense = dev.ReadCd(out cmdBuf, out _, lba, 96, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, false, false, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); if(!sense) { subBuf = DeinterleaveQ(cmdBuf); } else if(dbDev?.ATAPI?.RemovableMedias?.Any(d => d.SupportsPlextorReadCDDA == true) == true || dbDev?.SCSI?.RemovableMedias?.Any(d => d.SupportsPlextorReadCDDA == true) == true || dev.Manufacturer.ToLowerInvariant() == "plextor") sense = dev.PlextorReadCdDa(out cmdBuf, out _, lba, 96, 1, PlextorSubchannel.All, dev.Timeout, out _); { if(!sense) subBuf = DeinterleaveQ(cmdBuf); } } return sense; } /// Reads a Q16 subchannel sector for pregap calculation /// Device /// LBA /// Read subchannel /// Set if it is an audio track /// true if read correctly, false otherwise static bool GetSectorForPregapQ16(Device dev, uint lba, out byte[] subBuf, bool audioTrack) { byte[] cmdBuf; bool sense; subBuf = null; if(audioTrack) { sense = dev.ReadCd(out cmdBuf, out _, lba, 2368, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.Q16, dev.Timeout, out _); if(sense) sense = dev.ReadCd(out cmdBuf, out _, lba, 2368, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.Q16, dev.Timeout, out _); } else { sense = dev.ReadCd(out cmdBuf, out _, lba, 2368, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.Q16, dev.Timeout, out _); if(sense) sense = dev.ReadCd(out cmdBuf, out _, lba, 2368, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.Q16, dev.Timeout, out _); } if(!sense) { subBuf = new byte[16]; Array.Copy(cmdBuf, 2352, subBuf, 0, 16); } else { sense = dev.ReadCd(out cmdBuf, out _, lba, 16, 1, MmcSectorTypes.AllTypes, false, false, false, MmcHeaderCodes.None, false, false, MmcErrorField.None, MmcSubchannel.Q16, dev.Timeout, out _); if(sense) sense = dev.ReadCd(out cmdBuf, out _, lba, 16, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, false, false, MmcErrorField.None, MmcSubchannel.Q16, dev.Timeout, out _); if(!sense) subBuf = cmdBuf; } return sense; } /// De-interleaves Q subchannel /// Interleaved subchannel /// De-interleaved Q subchannel static byte[] DeinterleaveQ(byte[] subchannel) { int[] q = new int[subchannel.Length / 8]; // De-interlace Q subchannel for(int iq = 0; iq < subchannel.Length; iq += 8) { q[iq / 8] = (subchannel[iq] & 0x40) << 1; q[iq / 8] += subchannel[iq + 1] & 0x40; q[iq / 8] += (subchannel[iq + 2] & 0x40) >> 1; q[iq / 8] += (subchannel[iq + 3] & 0x40) >> 2; q[iq / 8] += (subchannel[iq + 4] & 0x40) >> 3; q[iq / 8] += (subchannel[iq + 5] & 0x40) >> 4; q[iq / 8] += (subchannel[iq + 6] & 0x40) >> 5; q[iq / 8] += (subchannel[iq + 7] & 0x40) >> 6; } byte[] deQ = new byte[q.Length]; for(int iq = 0; iq < q.Length; iq++) { deQ[iq] = (byte)q[iq]; } return deQ; } /// In place converts Q subchannel from binary to BCD numbering /// Q subchannel static void BinaryToBcdQ(byte[] q) { q[1] = (byte)(((q[1] / 10) << 4) + (q[1] % 10)); q[2] = (byte)(((q[2] / 10) << 4) + (q[2] % 10)); q[3] = (byte)(((q[3] / 10) << 4) + (q[3] % 10)); q[4] = (byte)(((q[4] / 10) << 4) + (q[4] % 10)); q[5] = (byte)(((q[5] / 10) << 4) + (q[5] % 10)); q[6] = (byte)(((q[6] / 10) << 4) + (q[6] % 10)); q[7] = (byte)(((q[7] / 10) << 4) + (q[7] % 10)); q[8] = (byte)(((q[8] / 10) << 4) + (q[8] % 10)); q[9] = (byte)(((q[9] / 10) << 4) + (q[9] % 10)); } /// In place converts Q subchannel from BCD to binary numbering /// Q subchannel static void BcdToBinaryQ(byte[] q) { q[1] = (byte)((q[1] / 16 * 10) + (q[1] & 0x0F)); q[2] = (byte)((q[2] / 16 * 10) + (q[2] & 0x0F)); q[3] = (byte)((q[3] / 16 * 10) + (q[3] & 0x0F)); q[4] = (byte)((q[4] / 16 * 10) + (q[4] & 0x0F)); q[5] = (byte)((q[5] / 16 * 10) + (q[5] & 0x0F)); q[6] = (byte)((q[6] / 16 * 10) + (q[6] & 0x0F)); q[7] = (byte)((q[7] / 16 * 10) + (q[7] & 0x0F)); q[8] = (byte)((q[8] / 16 * 10) + (q[8] & 0x0F)); q[9] = (byte)((q[9] / 16 * 10) + (q[9] & 0x0F)); } } }