void usb_handle_StandardInterfaceRequest(BDentry *bdp) { BYTE *packet = bdp->BDADDR; switch (packet[USB_bRequest]) { case USB_REQUEST_GET_STATUS: EP0_Inbdp->BDADDR[0] = 0x00; EP0_Inbdp->BDADDR[1] = 0x00; usb_ack_dat1(2); break; case USB_REQUEST_GET_INTERFACE: if (USB_NUM_INTERFACES > packet[USB_bInterface]) { // TODO: Implement alternative interfaces, or move responsibility to class/vendor functions. EP0_Inbdp->BDADDR[0] = 0; usb_ack_dat1(1); } else usb_RequestError(); break; case USB_REQUEST_SET_INTERFACE: if (USB_NUM_INTERFACES > packet[USB_bInterface] && 0u == packet[USB_wValue]) { // TODO: Implement alternative interfaces... usb_ack_dat1(0); } else usb_RequestError(); break; case USB_REQUEST_CLEAR_FEATURE: // JTR N/A for interface case USB_REQUEST_SET_FEATURE: // This is correct and finished code. default: usb_RequestError(); } }
void usb_handle_StandardInterfaceRequest(BDentry *bdp) { unsigned char *packet = bdp->BDADDR; switch (packet[USB_bRequest]) { case USB_REQUEST_GET_STATUS: rbdp->BDADDR[0] = 0x00; rbdp->BDADDR[1] = 0x00; usb_ack_dat1(rbdp, 2); // JTR common addition for STD and CLASS ACK // rbdp->BDCNT = 2; // rbdp->BDSTAT = UOWN + DTS + DTSEN; break; case USB_REQUEST_GET_INTERFACE: if (USB_NUM_INTERFACES > packet[USB_bInterface]) { // TODO: Implement alternative interfaces, or move responsibility to class/vendor functions. rbdp->BDADDR[0] = 0; usb_ack_dat1(rbdp, 1); // JTR common addition for STD and CLASS ACK } else usb_RequestError(); break; case USB_REQUEST_SET_INTERFACE: if (USB_NUM_INTERFACES > packet[USB_bInterface] && 0u == packet[USB_wValue]) { // TODO: Implement alternative interfaces... usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK } else usb_RequestError(); break; case USB_REQUEST_CLEAR_FEATURE: // JTR N/A for interface case USB_REQUEST_SET_FEATURE: // This is correct and finished code. default: usb_RequestError(); } }
void usb_handle_setup(void) { rbdp->BDSTAT = DTSEN; // Reclaim reply buffer switch (bdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_TypeMask) { case USB_bmRequestType_Standard: switch (bdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_RecipientMask) { case USB_bmRequestType_Device: usb_handle_StandardDeviceRequest(bdp); break; case USB_bmRequestType_Interface: usb_handle_StandardInterfaceRequest(bdp); break; case USB_bmRequestType_Endpoint: usb_handle_StandardEndpointRequest(bdp); break; default: usb_RequestError(); } break; case USB_bmRequestType_Class: if (class_setup_handler) class_setup_handler(); break; case USB_bmRequestType_Vendor: if (vendor_setup_handler) class_setup_handler(); break; default: usb_RequestError(); } /* Prepare endpoint for new reception */ bdp->BDCNT = USB_EP0_BUFFER_SIZE; // Size of EP0, should always be ep0? (JTR in practice YES) // JTR, is the next OUT transfer to be a setup packet (DAT0) or a DATA packet (DAT1)? // note that this is not an entirely robust way of doing things. See the microchip stack for // further comments and a better system as this does not account for errors and retries. // JTR Breakdown of what is happening here FYI. //bdp->BDSTAT = (!(bdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_PhaseMask) && // JTR meaning. If transfer was a CONTROL OUT then B7 (USB_bmRequestType_PhaseMask) of bmRequestType is '0' // && //bdp->BDADDR[USB_wLength] || bdp->BDADDR[USB_wLengthHigh]))? UOWN + DTS + DTSEN : UOWN + DTSEN; // If there is a CONTROL OUT DATA PACKET to follow then its count in (int) wLength will be != 0 // When both conditions are true then set for DAT1 (UOWN + DTS + DTSEN) // else set for DAT0 (UOWN + DTSEN) // See USB 2.0 spec 8.5.3 bdp->BDSTAT = (!(bdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_PhaseMask) && (bdp->BDADDR[USB_wLength] || bdp->BDADDR[USB_wLengthHigh])) ? UOWN + DTS + DTSEN : UOWN + DTSEN; // JTR Note. For rbdp after a setup packet, the standard or class request handler will force the correct DTS state // JTR Note. that CONTROL IN and OUT DATA packet transfers do not come back here and there is no // univesal way and place of setting up EP0 after these DATA transfers in this stack. // JTR Note. that there is a PIC18 silicon errata issue that this does not address by being here. See DS80220F-page 6 EnablePacketTransfer(); }
void cdc_setup(void) { BYTE *packet; size_t reply_len; packet = EP0_Outbdp->BDADDR; switch (packet[USB_bmRequestType] & (USB_bmRequestType_TypeMask | USB_bmRequestType_RecipientMask)) { case (USB_bmRequestType_Class | USB_bmRequestType_Interface): switch (packet[USB_bRequest]) { //JTR This is just a dummy, nothing defined to do for CDC ACM case CDC_SEND_ENCAPSULATED_COMMAND: usb_ack_dat1(0); break; //JTR This is just a dummy, nothing defined to do for CDC ACM case CDC_GET_ENCAPSULATED_RESPONSE: //usb_ack_zero(rbdp); usb_ack_dat1(0); break; case CDC_SET_COMM_FEATURE: // Optional case CDC_GET_COMM_FEATURE: // Optional case CDC_CLEAR_COMM_FEATURE: // Optional usb_RequestError(); // Not advertised in ACM functional descriptor break; case CDC_SET_LINE_CODING: // Optional, strongly recomended usb_set_out_handler(0, cdc_set_line_coding_data); // Register out handler function break; case CDC_GET_LINE_CODING: // Optional, strongly recomended // JTR reply length (7) is always going to be less than minimum EP0 size (8) reply_len = *((unsigned int *) &packet[USB_wLength]); if (sizeof (struct cdc_LineCodeing) < reply_len) { reply_len = sizeof (struct cdc_LineCodeing); } memcpy(EP0_Inbdp->BDADDR, (const void *) &linecodeing, reply_len); usb_ack_dat1(reply_len); // JTR common addition for STD and CLASS ACK usb_set_in_handler(0, cdc_get_line_coding); break; case CDC_SET_CONTROL_LINE_STATE: // Optional cls = *((struct _cdc_ControlLineState *) &packet[USB_wValue]); usb_set_in_handler(0, cdc_set_control_line_state_status); // JTR why bother? usb_ack_dat1(0); // JTR common addition for STD and CLASS ACK LineStateUpdated = 1; break; case CDC_SEND_BREAK: // Optional default: usb_RequestError(); } break; default: usb_RequestError(); } }
void usb_handle_setup(void) { EP0_Inbdp->BDSTAT = DTSEN; // Reclaim reply buffer EnablePacketTransfer(); // JTR this is placed here to overcome a errate issue with early PIC18 USB pics. switch (EP0_Outbdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_TypeMask) { case USB_bmRequestType_Standard: switch (EP0_Outbdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_RecipientMask) { case USB_bmRequestType_Device: usb_handle_StandardDeviceRequest(EP0_Outbdp); break; case USB_bmRequestType_Interface: usb_handle_StandardInterfaceRequest(EP0_Outbdp); break; case USB_bmRequestType_Endpoint: usb_handle_StandardEndpointRequest(EP0_Outbdp); break; default: usb_RequestError(); } break; case USB_bmRequestType_Class: if (class_setup_handler) class_setup_handler(); break; case USB_bmRequestType_Vendor: //ROBOTS FIX: http://dangerousprototypes.com/forum/viewtopic.php?f=39&t=3849&view=unread#unread // did call class_setup_handler(); if (vendor_setup_handler) vendor_setup_handler(); break; default: usb_RequestError(); } /* Prepare endpoint for new reception */ EP0_Outbdp->BDCNT = USB_EP0_BUFFER_SIZE; // Size of EP0, should always be ep0? // JTR, is the next OUT transfer to be a setup packet (DAT0) or a DATA packet (DAT1)? // note that this is not an entirely robust way of doing things. See the microchip stack for // further comments and a better system as this does not account for errors and retries // and it results in the SIE not accepting the final out ZLP status packet for IN transfers // with a data stage. However it works but it is not anything to be proud of... EP0_Outbdp->BDSTAT = (!(EP0_Outbdp->BDADDR[USB_bmRequestType] & USB_bmRequestType_PhaseMask) && (EP0_Outbdp->BDADDR[USB_wLength] || EP0_Outbdp->BDADDR[USB_wLengthHigh])) ? UOWN + DTS + DTSEN : UOWN + DTSEN; }
void usb_handle_StandardEndpointRequest(BDentry *bdp) { unsigned char *packet; unsigned char epnum; unsigned char dir; BDentry *epbd; packet = bdp->BDADDR; switch (packet[USB_bRequest]) { case USB_REQUEST_GET_STATUS: rbdp->BDADDR[0] = 0x00; // Assume no stall rbdp->BDADDR[1] = 0x00; // Same for stall or not epnum = packet[USB_wIndex] & 0x0F; dir = packet[USB_wIndex] >> 7; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_EVEN)]; if (epbd->BDSTAT &= ~BSTALL) rbdp->BDADDR[0] = 0x01; // EVEN BD is stall flag set? epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_ODD)]; if (epbd->BDSTAT &= ~BSTALL) rbdp->BDADDR[0] = 0x01; // ODD BD is stall flag set? usb_ack_dat1(rbdp, 2); // JTR common addition for STD and CLASS ACK break; case USB_REQUEST_CLEAR_FEATURE: epnum = packet[USB_wIndex] & 0x0F; dir = packet[USB_wIndex] >> 7; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_EVEN)]; epbd->BDSTAT &= ~BSTALL; if (dir) epbd->BDSTAT |= DTS; // JTR added IN EP set DTS as it will be toggled to zero next transfer if (0 == dir) epbd->BDSTAT &= ~DTS; // JTR added // JTR this pointless ATM. If ping-pong is enabled then you need to track PPBI // and set up ODD and EVEN BDs in respect to this. See complicated system in // microchip stack >= 2.8 // epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_ODD)]; // epbd->BDSTAT &= ~BSTALL; // if (dir) epbd->BDSTAT |= DTS; // JTR added // if (0 == dir) epbd->BDSTAT &= ~DTS; // JTR added usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK // rbdp->BDCNT = 0; // rbdp->BDSTAT = UOWN + DTS + DTSEN; break; case USB_REQUEST_SET_FEATURE: epnum = packet[USB_wIndex] & 0x0F; dir = packet[USB_wIndex] >> 7; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_EVEN)]; epbd->BDSTAT |= BSTALL; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_ODD)]; epbd->BDSTAT |= BSTALL; usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK // rbdp->BDCNT = 0; // rbdp->BDSTAT = UOWN + DTS + DTSEN; break; case USB_REQUEST_SYNCH_FRAME: default: usb_RequestError(); } }
void usb_handle_StandardDeviceRequest(BDentry *bdp) { unsigned char *packet = bdp->BDADDR; int i; switch (packet[USB_bRequest]) { case USB_REQUEST_GET_STATUS: rbdp->BDADDR[0] = usb_device_status & 0xFF; rbdp->BDADDR[1] = usb_device_status >> 8; // JTR I added usb_ack_dat1() simply so that the handling // of STANDARD and CLASS requests (in cdc.c) were consistant // rather than have the same thing done in two different ways. usb_ack_dat1(rbdp, 2); // JTR common addition for STD and CLASS ACK break; case USB_REQUEST_CLEAR_FEATURE: if (0x01u == packet[USB_wValue]) { // TODO: Remove magic (REMOTE_WAKEUP_FEATURE) usb_device_status &= ~0x0002; usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK } else usb_RequestError(); break; case USB_REQUEST_SET_FEATURE: if (0x01u == packet[USB_wValue]) { // TODO: Remove magic (REMOTE_WAKEUP_FEATURE) usb_device_status |= 0x0002; usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK } else usb_RequestError(); break; case USB_REQUEST_SET_ADDRESS: if (0x00u == packet[USB_wValueHigh] && 0x7Fu >= packet[USB_wValue]) { usb_addr_pending = packet[USB_wValue]; usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK usb_set_in_handler(0, usb_set_address); } else usb_RequestError(); break; case USB_REQUEST_GET_DESCRIPTOR: switch (packet[USB_bDescriptorType]) { case USB_DEVICE_DESCRIPTOR_TYPE: usb_desc_ptr = usb_device_descriptor; usb_desc_len = usb_device_descriptor[0]; if ((0 == packet[USB_wLengthHigh] && packet[USB_wLength] < usb_desc_ptr[0])) usb_desc_len = packet[USB_wLength]; break; case USB_CONFIGURATION_DESCRIPTOR_TYPE: if (packet[USB_bDescriptorIndex] >= usb_device_descriptor[17]) // TODO: remove magic usb_RequestError(); usb_desc_ptr = usb_config_descriptor; usb_desc_len = usb_desc_ptr[2] + usb_desc_ptr[3] * 256; for (i = 0; i < packet[USB_bDescriptorIndex]; i++) { // Implicit linked list traversal until requested configuration usb_desc_ptr += usb_desc_len; usb_desc_len = usb_desc_ptr[2] + usb_desc_ptr[3] * 256; } if ((packet[USB_wLengthHigh] < usb_desc_ptr[3]) || (packet[USB_wLengthHigh] == usb_desc_ptr[3] && packet[USB_wLength] < usb_desc_ptr[2])) usb_desc_len = packet[USB_wLength] + packet[USB_wLengthHigh] * 256; break; case USB_STRING_DESCRIPTOR_TYPE: // TODO: Handle language request. For now return standard language. if (packet[USB_bDescriptorIndex] >= usb_num_string_descriptors) usb_RequestError(); usb_desc_ptr = usb_string_descriptor; usb_desc_len = usb_desc_ptr[0]; for (i = 0; i < packet[USB_bDescriptorIndex]; i++) { // Implicit linked list traversal until requested configuration usb_desc_ptr += usb_desc_len; usb_desc_len = usb_desc_ptr[0]; } if ((0 == packet[USB_wLengthHigh] && packet[USB_wLength] < usb_desc_ptr[0])) usb_desc_len = packet[USB_wLength]; break; case USB_INTERFACE_DESCRIPTOR_TYPE: case USB_ENDPOINT_DESCRIPTOR_TYPE: default: usb_RequestError(); } usb_send_descriptor(); // Send first part of packet right away usb_set_in_handler(0, usb_send_descriptor); break; case USB_REQUEST_GET_CONFIGURATION: rbdp->BDADDR[0] = usb_current_cfg; usb_ack_dat1(rbdp, 1); // JTR common addition for STD and CLASS ACK // rbdp->BDCNT = 1; // rbdp->BDSTAT = UOWN + DTS + DTSEN; break; case USB_REQUEST_SET_CONFIGURATION: if (USB_NUM_CONFIGURATIONS >= packet[USB_wValue]) { // TODO: Support multiple configurations /* Configure endpoints (USB_UEPn - registers) */ usb_current_cfg = packet[USB_wValue]; if (usb_current_cfg != 0) { // JTR user_configured_init major addition. This is a CALLBACK to the USER when the device is enumerated. // This is when we setup non EP0 endpoints. // TODO: This really could be a function pointer usb_device_state = CONFIGURED_STATE; user_configured_init(); } else { usb_device_state = ADDRESS_STATE; } usb_ack_dat1(rbdp, 0); // JTR common addition for STD and CLASS ACK } else usb_RequestError(); break; case USB_REQUEST_SET_DESCRIPTOR: default: usb_RequestError(); } }
void usb_handle_StandardEndpointRequest(BDentry *bdp) { BYTE *packet; BYTE epnum; BYTE dir; BDentry *epbd; usb_uep_t *pUEP; packet = bdp->BDADDR; switch (packet[USB_bRequest]) { case USB_REQUEST_GET_STATUS: EP0_Inbdp->BDADDR[0] = 0x00; // Assume no stall EP0_Inbdp->BDADDR[1] = 0x00; // Same for stall or not epnum = packet[USB_wIndex] & 0x0F; dir = packet[USB_wIndex] >> 7; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_EVEN)]; if (epbd->BDSTAT &= ~BSTALL) EP0_Inbdp->BDADDR[0] = 0x01; // EVEN BD is stall flag set? //epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_ODD)]; //if (epbd->BDSTAT &= ~BSTALL) // rbdp->BDADDR[0] = 0x01; // ODD BD is stall flag set? usb_ack_dat1(2); break; case USB_REQUEST_CLEAR_FEATURE: // As this is really is an application event and there // should be a call back and protocol for handling the // possible lost of a data packet. // TODO: ping-ping support. epnum = packet[USB_wIndex] & 0x0F; // JTR Added V0.2 after microchip stuff up with their documentation. pUEP = USB_UEP; pUEP += epnum; *pUEP &= ~USB_UEP_EPSTALL; dir = packet[USB_wIndex] >> 7; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_EVEN)]; epbd->BDSTAT &= ~BSTALL; if (dir) epbd->BDSTAT |= DTS; // JTR added IN EP set DTS as it will be toggled to zero next transfer if (0 == dir) epbd->BDSTAT &= ~DTS; // JTR added // JTR this pointless ATM. If ping-pong is enabled then you need to track PPBI // and set up ODD and EVEN BDs in respect to this. See complicated system in // microchip stack >= 2.8 // epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_ODD)]; // epbd->BDSTAT &= ~BSTALL; // if (dir) epbd->BDSTAT |= DTS; // JTR added // if (0 == dir) epbd->BDSTAT &= ~DTS; // JTR added usb_ack_dat1(0); break; case USB_REQUEST_SET_FEATURE: epnum = packet[USB_wIndex] & 0x0F; dir = packet[USB_wIndex] >> 7; epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_EVEN)]; epbd->BDSTAT |= BSTALL; //epbd = &usb_bdt[USB_CALC_BD(epnum, dir, USB_PP_ODD)]; //epbd->BDSTAT |= BSTALL; usb_ack_dat1(0); break; case USB_REQUEST_SYNCH_FRAME: default: usb_RequestError(); } }
void usb_handle_StandardDeviceRequest(BDentry *bdp) { BYTE *packet = bdp->BDADDR; int i; switch (packet[USB_bRequest]) { case USB_REQUEST_GET_STATUS: EP0_Inbdp->BDADDR[0] = usb_device_status & 0xFF; EP0_Inbdp->BDADDR[1] = usb_device_status >> 8; usb_ack_dat1(2); break; case USB_REQUEST_CLEAR_FEATURE: if (0x01u == packet[USB_wValue]) { // TODO: Remove magic (REMOTE_WAKEUP_FEATURE) usb_device_status &= ~0x0002; usb_ack_dat1(0); } else usb_RequestError(); break; case USB_REQUEST_SET_FEATURE: if (0x01u == packet[USB_wValue]) { // TODO: Remove magic (REMOTE_WAKEUP_FEATURE) usb_device_status |= 0x0002; usb_ack_dat1(0); } else usb_RequestError(); break; case USB_REQUEST_SET_ADDRESS: if (0x00u == packet[USB_wValueHigh] && 0x7Fu >= packet[USB_wValue]) { usb_addr_pending = packet[USB_wValue]; usb_set_in_handler(0, usb_set_address); usb_ack_dat1(0); } else usb_RequestError(); break; case USB_REQUEST_GET_DESCRIPTOR: switch (packet[USB_bDescriptorType]) { case USB_DEVICE_DESCRIPTOR_TYPE: // There is only every one in pratice. usb_rom_ptr = usb_device_descriptor; usb_rom_len = usb_device_descriptor[0]; // Get BYTE length from descriptor always at byte [0] if ((0 == packet[USB_wLengthHigh] && packet[USB_wLength] < usb_rom_ptr[0])) usb_rom_len = packet[USB_wLength]; // If the HOST asked for LESS then must adjust count to the smaller number break; case USB_CONFIGURATION_DESCRIPTOR_TYPE: if (packet[USB_bDescriptorIndex] >= usb_device_descriptor[17]) { flag_usb_RequestError(); break; } usb_rom_ptr = usb_config_descriptor; usb_rom_len = usb_rom_ptr[2] + usb_rom_ptr[3] * 256; // Get WORD length from descriptor always at bytes 2&3 (Low-High) for (i = 0; i < packet[USB_bDescriptorIndex]; i++) { // Implicit linked list traversal until requested configuration usb_rom_ptr += usb_rom_len; usb_rom_len = usb_rom_ptr[2] + usb_rom_ptr[3] * 256; // Get (next) WORD length from descriptor always at bytes 2&3 (Low-High) } if ((packet[USB_wLengthHigh] < usb_rom_ptr[3]) || (packet[USB_wLengthHigh] == usb_rom_ptr[3] && packet[USB_wLength] < usb_rom_ptr[2])) usb_rom_len = packet[USB_wLength] + packet[USB_wLengthHigh] * 256; // If the HOST asked for LESS then must adjust count to the smaller number break; case USB_STRING_DESCRIPTOR_TYPE: // TODO: Handle language request. For now return standard language. if (packet[USB_bDescriptorIndex] >= usb_num_string_descriptors) { flag_usb_RequestError(); break; } usb_rom_ptr = usb_string_descriptor; usb_rom_len = usb_rom_ptr[0]; // Get BYTE length from descriptor always at byte [0] for (i = 0; i < packet[USB_bDescriptorIndex]; i++) { // Implicit linked list traversal until requested configuration usb_rom_ptr += usb_rom_len; usb_rom_len = usb_rom_ptr[0]; } if ((0 == packet[USB_wLengthHigh] && packet[USB_wLength] < usb_rom_ptr[0])) usb_rom_len = packet[USB_wLength]; break; case USB_INTERFACE_DESCRIPTOR_TYPE: case USB_ENDPOINT_DESCRIPTOR_TYPE: default: flag_usb_RequestError(); } if (0 == usbrequesterrorflag) { usb_send_rom(); // Send first part of packet right away, the rest is handled by the EP0 IN handler. usb_set_in_handler(0, usb_send_rom); } else { usb_RequestError(); } break; case USB_REQUEST_GET_CONFIGURATION: EP0_Inbdp->BDADDR[0] = usb_current_cfg; usb_ack_dat1(1); break; case USB_REQUEST_SET_CONFIGURATION: if (USB_NUM_CONFIGURATIONS >= packet[USB_wValue]) { // TODO: Support multiple configurations /* Configure endpoints (USB_UEPn - registers) */ usb_current_cfg = packet[USB_wValue]; if (usb_current_cfg != 0) { // JTR user_configured_init major addition. This is a CALLBACK to the USER when the device is enumerated. // This is when we setup non EP0 endpoints. // TODO: This really could be a function pointer usb_device_state = CONFIGURED_STATE; user_configured_init(); } else { usb_device_state = ADDRESS_STATE; } usb_ack_dat1(0); } else usb_RequestError(); break; case USB_REQUEST_SET_DESCRIPTOR: default: usb_RequestError(); } }