Sophie Dexter
/
Just4Trionic
Just4Trionic - CAN and BDM FLASH programmer for Saab cars
Diff: t8utils.cpp
- Revision:
- 5:1775b4b13232
- Parent:
- 4:682d96ff6d79
- Child:
- 6:2fbcbebed28c
--- a/t8utils.cpp Wed Sep 11 11:55:51 2013 +0000 +++ b/t8utils.cpp Sat Apr 25 17:07:08 2015 +0000 @@ -38,7 +38,7 @@ bool t8_show_VIN() { - uint16_t i; + uint32_t i; char T8TxFlo[] = T8FLOCTL; char T8TxMsg[] = T8REQVIN; char T8RxMsg[8]; @@ -87,7 +87,7 @@ char T8RxMsg[8]; char k = 0; -// GMLANTesterPresentT8(); +// GMLANTesterPresent(T8REQID, T8RESPID); // wait_ms(2000); // // printf("Requesting Security Access\r\n"); @@ -97,10 +97,10 @@ // } // printf("Security Access Granted\r\n"); // -// GMLANTesterPresentT8(); +// GMLANTesterPresent(T8REQID, T8RESPID); // wait_ms(2000); // -// GMLANTesterPresentT8(); +// GMLANTesterPresent(T8REQID, T8RESPID); // wait_ms(2000); // if (!can_send_timeout (T8TSTRID, SetVin10, 8, T8MESSAGETIMEOUT)) { @@ -132,7 +132,7 @@ for (k = 0; k < 8; k++ ) printf("0x%02X ", T8RxMsg[k] ); printf("\r\n"); return TRUE; -// GMLANTesterPresentT8(); +// GMLANTesterPresent(T8REQID, T8RESPID); // wait_ms(2000); // } @@ -147,13 +147,25 @@ // return: bool TRUE if there was a message, FALSE if no message. // -bool t8_authenticate(char level) +bool t8_authenticate(uint32_t ReqID, uint32_t RespID, char level) { - uint16_t seed, key; - if (!GMLANSecurityAccessRequest(level, seed)) { + uint16_t i, seed, key; +// if (!GMLANSecurityAccessRequest(ReqID, RespID, level, seed)) { +// printf("Unable to request SEED value for security access\r\n"); +// return FALSE; +// } + + for (i=0; i < 20; i++) { + if (GMLANSecurityAccessRequest(ReqID, RespID, level, seed)) + break; + wait(1); + GMLANTesterPresent(ReqID, RespID); + } + if (i == 20) { printf("Unable to request SEED value for security access\r\n"); return FALSE; } + if ( seed == 0x0000 ) { printf("T8 ECU is already unlocked\r\n"); return TRUE; @@ -175,16 +187,17 @@ key = (key >> 8) | (key << 8); key -= 0x3FC7; */ - if (!GMLANSecurityAccessSendKey(level, key)) { + wait_ms(1); + if (!GMLANSecurityAccessSendKey(ReqID, RespID, level, key)) { printf("Unable to send KEY value for security access\r\n"); return FALSE; } printf("Key Accepted\r\n"); + wait_ms(500); // was 5 return TRUE; } - // // t8_dump // @@ -197,7 +210,7 @@ bool t8_dump() { - uint16_t i = 0, k = 0; + uint32_t i = 0, k = 0; char T8TxMsg[8]; char T8RxMsg[8]; @@ -206,28 +219,19 @@ printf("Creating FLASH dump file...\r\n"); // - GMLANTesterPresentT8(); -// - if (!GMLANprogrammingSetupProcess()) + if (!GMLANprogrammingSetupProcess(T8REQID, T8RESPID)) return FALSE; // - wait_ms(500); printf("Requesting Security Access\r\n"); - if (!t8_authenticate(0x01)) { + if (!t8_authenticate(T8REQID, T8RESPID, 0x01)) { printf("Unable to get Security Access\r\n"); return FALSE; } printf("Security Access Granted\r\n"); - wait_ms(500); - GMLANTesterPresentT8(); // - char BootLoader[] = T8Bootloader; - if(!GMLANprogrammingUtilityFileProcess(BootLoader)) + if(!GMLANprogrammingUtilityFileProcess(T8REQID, T8RESPID, T8BootloaderRead)) return FALSE; // - uint32_t StartAddress = 0x0; - uint16_t txpnt = 0; - char iFrameNumber = 0x21; // printf("Downloading FLASH BIN file...\r\n"); printf("Creating FLASH dump file...\r\n"); @@ -238,7 +242,34 @@ } printf(" 0.00 %% complete.\r"); TesterPresent.start(); - while (StartAddress < 0x100000) { // 0x100000 + +// It is possible to save some time by only reading the program code and CAL data +// This is just a rough calculation, and slight overestimate of the number of blocks of data needed to send the BIN file + T8TxMsg[0] = 0x06; + T8TxMsg[1] = 0x21; + T8TxMsg[2] = 0x80; // Blocksize + T8TxMsg[3] = 0x00; // This address (0x020140) points to the Header at the end of the BIN + T8TxMsg[4] = 0x02; + T8TxMsg[5] = 0x01; + T8TxMsg[6] = 0x40; + T8TxMsg[7] = 0xaa; + if (!can_send_timeout (T8TSTRID, T8TxMsg, 7, T8MESSAGETIMEOUT)) { + printf("Unable to download FLASH\r\n"); + return FALSE; + } + if (!can_wait_timeout(T8ECU_ID, T8RxMsg, 8, T8MESSAGETIMEOUT)) + return FALSE; + uint32_t EndAddress = (T8RxMsg[5] << 16) | (T8RxMsg[6] << 8) | T8RxMsg[7]; + EndAddress += 0x200; // Add some bytes for the Footer itself and to account for division rounded down later + char T8TxFlo[] = T8FLOCTL; + can_send_timeout (T8TSTRID, T8TxFlo, 8, T8MESSAGETIMEOUT); + for (i = 0; i < 0x12; i++) { + if (!can_wait_timeout(T8ECU_ID, T8RxMsg, 8, T8MESSAGETIMEOUT)) + return FALSE; + } + printf("Reading your BIN file adjusted for footer = 0x%06X Bytes\r\n", EndAddress ); + + for ( uint32_t StartAddress = 0x0; StartAddress < EndAddress; StartAddress +=0x80 ) { // 0x100000 T8TxMsg[0] = 0x06; T8TxMsg[1] = 0x21; T8TxMsg[2] = 0x80; // Blocksize @@ -258,26 +289,22 @@ return FALSE; #ifdef DEBUG printf("first %#.3f\r\n",timer.read()); - for (k = 0; k < 8; k++ ) printf("0x%02X ", T8RxMsg[k] ); - printf("\r\n"); #endif - txpnt = 0; + uint32_t txpnt = 0; for (k = 4; k < 8; k++ ) file_buffer[txpnt++] = T8RxMsg[k]; uint8_t DataFrames = 0x12; - iFrameNumber = 0x21; + char iFrameNumber = 0x21; char T8TxFlo[] = T8FLOCTL; + can_send_timeout (T8TSTRID, T8TxFlo, 8, T8MESSAGETIMEOUT); #ifdef DEBUG printf("flowCtrl %#.3f\r\n",timer.read()); #endif - can_send_timeout (T8TSTRID, T8TxFlo, 8, T8MESSAGETIMEOUT); for (i = 0; i < DataFrames; i++) { if (!can_wait_timeout(T8ECU_ID, T8RxMsg, 8, T8MESSAGETIMEOUT)) return FALSE; #ifdef DEBUG printf("Consec %#.3f\r\n",timer.read()); - for (k = 0; k < 8; k++ ) printf("0x%02X ", T8RxMsg[k] ); - printf("\r\n"); #endif iFrameNumber++; for (k = 1; k < 8; k++ ) file_buffer[txpnt++] = T8RxMsg[k]; @@ -288,12 +315,22 @@ printf ("Error writing to the FLASH BIN file.\r\n"); return TERM_ERR; } - StartAddress +=0x80; - printf("%6.2f\r", (100.0*(float)StartAddress)/(float)(0x100000) ); + printf("%6.2f\r", (100.0*(float)StartAddress)/(float)(EndAddress) ); if (TesterPresent.read_ms() > 2000) { - GMLANTesterPresentT8(); + GMLANTesterPresent(T8REQID, T8RESPID); TesterPresent.reset(); - ACTIVITYLEDON; + } + } + + for (uint32_t i = 0; i < 0x80; i++) + file_buffer[i] = 0xFF; + while ( ftell(fp) < 0x100000 ) { +// for ( uint32_t StartAddress = EndAddress; StartAddress < 0x100000; StartAddress +=0x80 ) { + fwrite((file_buffer), 1, 0x80, fp); + if (ferror (fp)) { + fclose (fp); + printf ("Error writing to the FLASH BIN file.\r\n"); + return TERM_ERR; } } @@ -304,69 +341,44 @@ return TRUE; } -bool t8_erase() -{ - printf("Erasing T8 ECU FLASH...\r\n"); - printf("SUCCESS: The FLASH has been erased.\r\n"); - return TRUE; -} - -bool t8_flash_raw() -{ - timer.reset(); - timer.start(); - printf("Checking the FLASH BIN file...\r\n"); - timer.stop(); - printf("SUCCESS! Programming the FLASH took %#.1f seconds.\r\n",timer.read()); - return TRUE; -} - bool t8_flash() { - uint16_t i = 0, j = 0, k = 0; + uint32_t i = 0, j = 0, k = 0; timer.reset(); timer.start(); printf("FLASHing T8 BIN file...\r\n"); // - GMLANTesterPresentT8(); -// - if (!GMLANprogrammingSetupProcess()) + if (!GMLANprogrammingSetupProcess(T8REQID, T8RESPID)) return FALSE; // - wait_ms(500); printf("Requesting Security Access\r\n"); - if (!t8_authenticate(0x01)) { + if (!t8_authenticate(T8REQID, T8RESPID, 0x01)) { printf("Unable to get Security Access\r\n"); return FALSE; } printf("Security Access Granted\r\n"); - wait_ms(500); - GMLANTesterPresentT8(); // - char BootLoader[] = T8BootloaderProg; - if(!GMLANprogrammingUtilityFileProcess(BootLoader)) +// const uint8_t BootLoader[] = T8BootloaderProg; +// if(!GMLANprogrammingUtilityFileProcess(T8REQID, T8RESPID, BootLoader)) + if(!GMLANprogrammingUtilityFileProcess(T8REQID, T8RESPID, T8BootLoaderWrite)) return FALSE; // - - // All steps needed to transfer and start a bootloader ('Utility File' in GMLAN parlance) -// bool GMLANprogrammingUtilityFileProcess(char UtilityFile[]) { -// uint16_t i = 0, j = 0, k = 0; uint32_t StartAddress = 0x020000; - uint16_t txpnt = 0; + uint32_t txpnt = 0; char iFrameNumber = 0x21; char GMLANMsg[8]; char data2Send[0xE0]; // - // fopen modified.hex here, check it is OK and work out how much data I need to send + // fopen modified.bin here, check it is OK and work out how much data I need to send // need lots of fcloses though printf("Checking the FLASH BIN file...\r\n"); - FILE *fp = fopen("/local/modified.hex", "r"); // Open "modified.hex" on the local file system for reading + FILE *fp = fopen("/local/modified.bin", "r"); // Open "modified.bin" on the local file system for reading if (!fp) { - printf("Error: I could not find the BIN file MODIFIED.HEX\r\n");; + printf("Error: I could not find the BIN file MODIFIED.BIN\r\n");; return TERM_ERR; } // obtain file size - it should match the size of the FLASH chips: @@ -376,7 +388,10 @@ // read the initial stack pointer value in the BIN file - it should match the value expected for the type of ECU uint32_t stack_long = 0; - if (!fread(&stack_long,4,1,fp)) return TERM_ERR; + if (!fread(&stack_long,4,1,fp)) { + fclose(fp); + return TERM_ERR; + } stack_long = (stack_long >> 24) | ((stack_long << 8) & 0x00FF0000) | ((stack_long >> 8) & 0x0000FF00) | (stack_long << 24); // if (file_size != T8FLASHSIZE || stack_long != T8POINTER) { @@ -389,7 +404,10 @@ // This is just a rough calculation, and slight overestimate of the number of blocks of data needed to send the BIN file uint32_t blocks2Send; fseek(fp,0x020140,SEEK_SET); - if (!fread(&blocks2Send,4,1,fp)) return TERM_ERR; + if (!fread(&blocks2Send,4,1,fp)) { + fclose(fp); + return TERM_ERR; + } blocks2Send = (blocks2Send >> 24) | ((blocks2Send << 8) & 0x00FF0000) | ((blocks2Send >> 8) & 0x0000FF00) | (blocks2Send << 24); printf("Start address of BIN file's Footer area = 0x%06X\r\n", blocks2Send ); blocks2Send += 0x200; // Add some bytes for the Footer itself and to account for division rounded down later @@ -401,45 +419,31 @@ fseek (fp , 0x020000 , SEEK_SET); // Erase the FLASH printf("Waiting for FLASH to be Erased\r\n"); - if (!GMLANRequestDownload(GMLANRequestDownloadModeEncrypted)) { + if (!GMLANRequestDownload(T8REQID, T8RESPID, GMLANRequestDownloadModeEncrypted)) { + fclose(fp); printf("Unable to erase the FLASH chip!\r\n"); return FALSE; } // Now send the BIN file + GMLANTesterPresent(T8REQID, T8RESPID); TesterPresent.start(); printf("Sending FLASH BIN file\r\n"); printf(" 0.00 %% complete.\r"); for (i=0; i<blocks2Send; i++) { // get a block of 0xE0 bytes in an array called data2Send - for ( j = 0; j < 0xE0; j++ ) { - //data[k] = *(bin + bin_count++); - if (!fread(&data2Send[j],1,1,fp)) { - fclose(fp); - printf("Error reading the BIN file MODIFIED.HEX\r\n"); - return FALSE; - } - // encrypt data2Send array by XORing with 6 different in a ring (modulo function) - switch ( ((0xE0*i)+j) %6) { - case 1: - data2Send[j] ^= 0x68; - break; - case 2: - data2Send[j] ^= 0x77; - break; - case 3: - data2Send[j] ^= 0x6D; - break; - case 4: - data2Send[j] ^= 0x47; - break; - default: // remainder 0 and 5 both XOR with 0x39 - data2Send[j] ^= 0x39; - } + if (!fread(data2Send,0xE0,1,fp)) { + fclose(fp); + printf("\r\nError reading the BIN file MODIFIED.BIN\r\n"); + return FALSE; } + // encrypt data2Send array by XORing with 6 different values in a ring (modulo function) + char key[6] = { 0x39, 0x68, 0x77, 0x6D, 0x47, 0x39 }; + for ( j = 0; j < 0xE0; j++ ) + data2Send[j] ^= key[(((0xE0*i)+j) % 6)]; // Send the block of data - if (!GMLANDataTransferFirstFrame(0xE6, GMLANDOWNLOAD, StartAddress)) { + if (!GMLANDataTransferFirstFrame(T8REQID, T8RESPID, 0xE6, GMLANDOWNLOAD, StartAddress)) { fclose(fp); - printf("Unable to start BIN File Upload\r\n"); + printf("\r\nUnable to start BIN File Upload\r\n"); return FALSE; } // Send 0x20 messages of 0x07 bytes each (0x20 * 0x07 = 0xE0) @@ -449,50 +453,21 @@ GMLANMsg[0] = iFrameNumber; for (k=1; k<8; k++) GMLANMsg[k] = data2Send[txpnt++]; -#ifdef DEBUG - for (k = 0; k < 8; k++ ) printf("0x%02X ", GMLANMsg[k] ); - printf("\r\n"); -#endif - if (!can_send_timeout(T8RequestId, GMLANMsg, 8, GMLANPTCT)) { + if (!can_send_timeout(T8REQID, GMLANMsg, 8, GMLANPTCT)) { fclose(fp); - printf("Unable to send BIN File\r\n"); + printf("\r\nUnable to send BIN File\r\n"); return FALSE; } ++iFrameNumber &= 0x2F; -// wait_ms(1); - wait_us(250); - } - if (!can_wait_timeout(T8ResponseId, GMLANMsg, 8, GMLANPTCT)) { - fclose(fp); - printf("I did not receive a block acknowledge message\r\n"); - return FALSE; + wait_us(1000); // can be as low as 250 for an ECU on its own, but need 1000 (1ms) to work in a car (use a longer delay to be ultrasafe??? ) } - while (GMLANMsg[0] == 0x03 && GMLANMsg[1] == 0x7F && GMLANMsg[2] == 0x36 && GMLANMsg[3] == 0x78) { - printf("I'm waiting for a Block to be programmed into FLASH\r\n"); - if (!can_wait_timeout(T8ResponseId, GMLANMsg, 8, GMLANPTCTENHANCED)) { - printf("I did not receive a block acknowledge message after enhanced timeout\r\n"); - fclose(fp); - return FALSE; - } - } -#ifdef DEBUG - for (k = 0; k < 8; k++ ) printf("0x%02X ", GMLANMsg[k] ); - printf("\r\n"); -#endif - if ( GMLANMsg[0] == 0x03 && GMLANMsg[1] == 0x7F && GMLANMsg[2] == 0x36 ) { - GMLANShowReturnCode(GMLANMsg[3]); - fclose(fp); - return FALSE; - } - if (GMLANMsg[0] != 0x01 && GMLANMsg[1] != 0x76) { - printf("EXITING due to an unexpected CAN message"); + if (!GMLANDataTransferBlockAcknowledge(T8RESPID)) { fclose(fp); return FALSE; } if (TesterPresent.read_ms() > 2000) { - GMLANTesterPresentT8(); + GMLANTesterPresent(T8REQID, T8RESPID); TesterPresent.reset(); - ACTIVITYLEDON; } StartAddress += 0xE0; printf("%6.2f\r", (100.0*(float)i)/(float)(blocks2Send) ); @@ -500,14 +475,158 @@ // FLASHing complete printf("%6.2f\r\n", (float)100 ); // End programming session and return to normal mode - if (!GMLANReturnToNormalMode()) { + if (!GMLANReturnToNormalMode(T8REQID, T8RESPID)) { fclose(fp); printf("UH-OH! T8 ECU did not Return To Normal Mode!!\r\n"); return FALSE; } - wait_ms(1000); timer.stop(); printf("SUCCESS! FLASHing the BIN file took %#.1f seconds.\r\n",timer.read()); fclose(fp); return TRUE; } + +bool t8_recover() +{ + uint32_t i = 0, j = 0, k = 0; + + timer.reset(); + timer.start(); + printf("Recovering your T8 ECU ...\r\n"); +// + if (!GMLANprogrammingSetupProcess(T8USDTREQID, T8UUDTRESPID)) + return FALSE; +// + printf("Requesting Security Access\r\n"); + if (!t8_authenticate(T8USDTREQID, T8UUDTRESPID, 0x01)) { + printf("Unable to get Security Access\r\n"); + return FALSE; + } + printf("Security Access Granted\r\n"); +// +// const uint8_t BootLoader[] = T8BootloaderProg; +// if(!GMLANprogrammingUtilityFileProcess(T8USDTREQID, T8UUDTRESPID, BootLoader)) + if(!GMLANprogrammingUtilityFileProcess(T8USDTREQID, T8UUDTRESPID, T8BootLoaderWrite)) + return FALSE; +// + + +// All steps needed to transfer and start a bootloader ('Utility File' in GMLAN parlance) + uint32_t StartAddress = 0x020000; + uint32_t txpnt = 0; + char iFrameNumber = 0x21; + char GMLANMsg[8]; + char data2Send[0xE0]; +// + // fopen modified.bin here, check it is OK and work out how much data I need to send + // need lots of fcloses though + printf("Checking the FLASH BIN file...\r\n"); + FILE *fp = fopen("/local/modified.bin", "r"); // Open "modified.bin" on the local file system for reading + if (!fp) { + printf("Error: I could not find the BIN file MODIFIED.BIN\r\n");; + return TERM_ERR; + } + // obtain file size - it should match the size of the FLASH chips: + fseek (fp , 0 , SEEK_END); + uint32_t file_size = ftell (fp); + rewind (fp); + + // read the initial stack pointer value in the BIN file - it should match the value expected for the type of ECU + uint32_t stack_long = 0; + if (!fread(&stack_long,4,1,fp)) { + fclose(fp); + return TERM_ERR; + } + stack_long = (stack_long >> 24) | ((stack_long << 8) & 0x00FF0000) | ((stack_long >> 8) & 0x0000FF00) | (stack_long << 24); +// + if (file_size != T8FLASHSIZE || stack_long != T8POINTER) { + fclose(fp); + printf("The BIN file does not appear to be for a T8 ECU :-(\r\n"); + printf("BIN file size: %#010x, FLASH chip size: %#010x, Pointer: %#010x.\r\n", file_size, T7FLASHSIZE, stack_long); + return TERM_ERR; + } +// It is possible to save some time by only sending the program code and CAL data +// This is just a rough calculation, and slight overestimate of the number of blocks of data needed to send the BIN file + uint32_t blocks2Send; + fseek(fp,0x020140,SEEK_SET); + if (!fread(&blocks2Send,4,1,fp)) { + fclose(fp); + return TERM_ERR; + } + blocks2Send = (blocks2Send >> 24) | ((blocks2Send << 8) & 0x00FF0000) | ((blocks2Send >> 8) & 0x0000FF00) | (blocks2Send << 24); + printf("Start address of BIN file's Footer area = 0x%06X\r\n", blocks2Send ); + blocks2Send += 0x200; // Add some bytes for the Footer itself and to account for division rounded down later + blocks2Send -= 0x020000; // Remove 0x020000 because we don't send the bootblock and adaptation blocks + printf("Amount of data to send BIN file adjusted for footer = 0x%06X Bytes\r\n", blocks2Send ); + blocks2Send /= 0xE0; + printf("Number of Blocks of 0xE0 Bytes needed to send BIN file = 0x%04X\r\n", blocks2Send ); +// Move BIN file pointer to start of data + fseek (fp , 0x020000 , SEEK_SET); +// Erase the FLASH + printf("Waiting for FLASH to be Erased\r\n"); + if (!GMLANRequestDownload(T8REQID, T8RESPID, GMLANRequestDownloadModeEncrypted)) { + fclose(fp); + printf("Unable to erase the FLASH chip!\r\n"); + return FALSE; + } +// Now send the BIN file + GMLANTesterPresent(T8REQID, T8RESPID); + TesterPresent.start(); + printf("Sending FLASH BIN file\r\n"); + printf(" 0.00 %% complete.\r"); + for (i=0; i<blocks2Send; i++) { + // get a block of 0xE0 bytes in an array called data2Send + if (!fread(data2Send,0xE0,1,fp)) { + fclose(fp); + printf("\r\nError reading the BIN file MODIFIED.BIN\r\n"); + return FALSE; + } + // encrypt data2Send array by XORing with 6 different values in a ring (modulo function) + char key[6] = { 0x39, 0x68, 0x77, 0x6D, 0x47, 0x39 }; + for ( j = 0; j < 0xE0; j++ ) + data2Send[j] ^= key[(((0xE0*i)+j) % 6)]; + // Send the block of data + if (!GMLANDataTransferFirstFrame(T8REQID, T8RESPID, 0xE6, GMLANDOWNLOAD, StartAddress)) { + fclose(fp); + printf("\r\nUnable to start BIN File Upload\r\n"); + return FALSE; + } + // Send 0x20 messages of 0x07 bytes each (0x20 * 0x07 = 0xE0) + txpnt = 0; + iFrameNumber = 0x21; + for (j=0; j < 0x20; j++) { + GMLANMsg[0] = iFrameNumber; + for (k=1; k<8; k++) + GMLANMsg[k] = data2Send[txpnt++]; + if (!can_send_timeout(T8REQID, GMLANMsg, 8, GMLANPTCT)) { + fclose(fp); + printf("\r\nUnable to send BIN File\r\n"); + return FALSE; + } + ++iFrameNumber &= 0x2F; + wait_us(1000); // was 250 use 1000 or wait_ms(1) to be ultrasafe + } + if (!GMLANDataTransferBlockAcknowledge(T8RESPID)) { + fclose(fp); + return FALSE; + } + if (TesterPresent.read_ms() > 2000) { + GMLANTesterPresent(T8REQID, T8RESPID); + TesterPresent.reset(); + } + StartAddress += 0xE0; + printf("%6.2f\r", (100.0*(float)i)/(float)(blocks2Send) ); + } +// FLASHing complete + printf("%6.2f\r\n", (float)100 ); +// End programming session and return to normal mode + if (!GMLANReturnToNormalMode(T8REQID, T8RESPID)) { + fclose(fp); + printf("UH-OH! T8 ECU did not Return To Normal Mode!!\r\n"); + return FALSE; + } + timer.stop(); + fclose(fp); + printf("SUCCESS: Your T8 ECU has been recovered.\r\n"); + return TRUE; +}