stm32cube, usb host, hid mouse/keyboard - usb

I've been trying to set up some basic usb comunication with a usb mouse using my stm32f4 discvery. there are no usb examples for my board so I went in to look at other boards.
stm's Cube library thingy promissed easy development and all, so I generated a project with cube inlcuding USB_OTG_FS in host only mode and four IO pins for LED's.
I had a look in the files and it seemed like reading mouse buttons would be fairly trivial, but I can't make it work.
if I run
devtype = USBH_HID_GetDeviceType(&hUsbHostFS);
it detects the device correctly.
USBH_HID_MouseInit(&hUsbHostFS)
returns USBH_OK,
but no matter what I try
mouse=USBH_HID_GetMouseInfo(&hUsbHostFS);
is always NULL
"USBH_HID_MouseInit" is never mentioned in the documentation(DM00105256.pdf)
also, only the RTOS examples use them. Reading the usb host standalone example, it would seem that to comunicate with a mouse would be as simple as runing GetDeviceType, and poll GetMouseInfo, but I can't get it to work.
this is all the code I added, it's executed in the main while
if(Appli_state == APPLICATION_READY){
switch(state)
{
case 0:
devtype = USBH_HID_GetDeviceType(&hUsbHostFS);
if(devtype == HID_MOUSE){
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,1);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_12,0);
state=1;
}
else if(devtype == HID_KEYBOARD){
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,0);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_12,1);
state=2;
}
else{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,0);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_12,0);
}
break;
case 1:
if(USBH_HID_MouseInit(&hUsbHostFS)==0){
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_15,1);
state=3;
}
else {
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_15,0);
state=0;
}
break;
case 3:
mouse=USBH_HID_GetMouseInfo(&hUsbHostFS);
if(mouse != NULL)HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,1);
else HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,0);
break;
default:
break;
}
}
if(Appli_state != APPLICATION_READY) state=0;
rest of the code is unchanged. like I said before it detects the connection and if it's a mouse, keyboard or unknown device just fine. I just can't get it to communicate

The Cube is buggy, coded in a strange way, and not well documented.
Depending on the actual board you have, you must make sure the clocks are setup correctly (in system_stm32f4.c)
With the stm32f407-based version (http://www.st.com/en/evaluation-tools/stm32f4discovery.html) you should use these settings:
HSE = 8000000
PLL_M = 8
PLL_Q = 7
PLL_N = 336
PLL_P = 4
The USB OTG FS requires a 48MHz clock. Here are the equations:
USB OTG FS, SDIO and RNG Clock = PLL_VCO / PLLQ
PLL_VCO = (HSE_VALUE / PLL_M) * PLL_N

Related

STM32 USB CDC some data lost with Win 10

I use USB device - STMicroelectronics development board. Use firmware, that support usb hardware. Its works as USB serial port.
On host PC (win10 21H1) i use serial terminal ("Tera Term") for get data from my device. I use standart windows usbserial driver.
My device sending data. If data flow is small (1-2-5 kByte/s) - all work fine. But if i speed up (flow about 100 kByte/s or more) - i see data loss.
I communicated with STMicroelectronics support. We checked issue. We saw USB communication with USB analyzer. We think, than it's windows side problem.
Also, I use a custom port read utility. Data integrity problem persists.
In received data i saw lost 64 or 128... multiple of 64 bytes. 64bytes - endpoint size in my case. See linked data for more information.
I create USB_test project in CubeMx. And add simple code for sending data to PC. Loop data sending if previous CDC transmit complete. Adding delays is unacceptable: firstly, it is not 100% elimination of losses; secondly, it has a bad effect on the bandwidth of the channel.
//in main() function
uint8_t is_transmit = 0;
HAL_Delay(5000);
uint8_t Buf[2048];
uint8_t k = 48;
// fill the array with printable characters
for(uint16_t i=0; i<sizeof(Buf)-2; i++){
if(k > 100) {
k = 48;
}
Buf[i] = k++;
}
// array - is a one string line
Buf[sizeof(Buf)-2] = '\r';
Buf[sizeof(Buf)-1] = '\n';
while (1)
{
if(is_transmit == 0){
is_transmit = 1;
//HAL_Delay(1); // add delay on 1 ms reduces the likelihood of losses by an order of magnitude
CDC_Transmit_FS(Buf, sizeof(Buf));
}
}
In CDC_TransmitCplt_FS() i flash is_transmit.
static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum)
{
---
extern uint8_t is_transmit;
is_transmit = 0;
---
return result;
}
Information from ST support communication and USB analyzer log file.
https://drive.google.com/drive/folders/1CvTPfaFGmcFxD4V5zTvsVE6U26DNwG2v?usp=sharing
How i fix this issue? I need data flow from device to host 500 kB/s or more.
Best regards, Andrey.

STM32 USB programming, jump to bootloader for DFU

I was having trouble on the STM32L462xx with setting the device up for flashing over USB without having access to the BOOT0 pin. Going off of the tutorial on the ST site didn't seem to accomplish the task. Has anyone successfully set the STM32L4 into bootloader mode from software?
https://stm32f4-discovery.net/2017/04/tutorial-jump-system-memory-software-stm32/
I was able to set the device into a mode which it can be programmed with STM32Cube Programmer or the DFU-util program using the following code. This is partly a signal boost for this programmers solution which went against the ST tutorial on their site saying how to put jump the device memory to bootloader for USB programming
https://github.com/markusgritsch/SilF4ware/blob/94bb679119a0b9879fedc62a5e22f40623433242/SilF4ware/drv_reset.c
void jump_to_bootloader(void)
{
__enable_irq();
HAL_RCC_DeInit();
HAL_DeInit();
SysTick->CTRL = SysTick->LOAD = SysTick->VAL = 0;
__HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH();
const uint32_t p = (*((uint32_t *) 0x1FFF0000));
__set_MSP( p );
void (*SysMemBootJump)(void);
SysMemBootJump = (void (*)(void)) (*((uint32_t *) 0x1FFF0004));
SysMemBootJump();
while( 1 ) {}
}

bluetooth background mode IOS when the screen is locked

I would like to implement background bluetooth scanning on IOS. When the application goes in background mode it calls TestCentralManagerDelegate which implements DiscoveredPeripheral function. It is triggered when a new bluetooth peripheral device is detected. If a new bluetooth device is detected the application read the manufacture data which is presented in Dictionary advertisementData (as argument of DiscoveredPeripheral function). The manufacture data are obtained by calling ManufactureData = advertisementData["kCBAdvDataManufacturerData"].ToString(). The discovering of the manufacture data was tested on two different iPhones 5s and 6 with the same iOS 12.1. When the application goes in background mode, I locked the screen.
In the case of iPhone 5s, I observed that ManufactureData was found each time
the DiscoveredPeripheral function is triggered. This fact is not true for iPhone 6, each time I got ManufactureData = null. It is worth mentioning that the manufacture data are received in both cases if the screen is not locked.
I do not understand why the iPhone 6 does not find ManufactureData, meanwhile the iPhone 5s does. I would accept the fact that phones have different operating systems and this implies different responses, but in my case this is not the case. I will appreciate any help for better understanding aforementioned problem.
Here is code I am using Xamarin.iOS.
public override void DiscoveredPeripheral(CBCentralManager central, CBPeripheral peripheral, NSDictionary advertisementData, NSNumber RSSI)
{
try
{
central.StopScan();
if (peripheral == null || advertisementData == null)
{
central.ScanForPeripherals(cbuuids);
return;
}
string ManufactureData;
if (advertisementData.ContainsKey(new NSString("kCBAdvDataManufacturerData")))
{
ManufactureData = advertisementData["kCBAdvDataManufacturerData"].ToString();
}
else
{
ManufactureData = null;
CrossLocalNotifications.Current.Show("no advertising data", "no advertising data", 10);
central.ScanForPeripherals(cbuuids);
return;
}
central.ScanForPeripherals(cbuuids);
}
catch
{
central.ScanForPeripherals(cbuuids);
}
}

Atmel SAM D21 DMA stuck in busy state when using USB

I'm using the SAMD21 xPlained pro with a prototype to acquire a signal, using the internal ADC and DMAC which configured to collect 5500 samples.
The samples then transferred to a PC application via the target USB.
The firmware that I wrote for that is working, but I noticed that from time to time the DMA get stuck in busy state.
To debug that I canceled the transfer part to the PC via the USB.
I noticed that DMA works fine and transfers the samples to the memory, but if I connect the USB cable to the PC (without transferring data through the USB), the DMA gets stuck from time to time;
but once I disconnect the (target) USB cable, the DMA works continuously without getting stuck in busy state.
I suspect that it has something with the interrupt and priorities of the USB and the ADC, which are both using the DMA.
I thought I should set the ADC sampling at top priority so the USB won't cause the DMA stuck in busy state, but I couldn't find how to configure that in the code (I'm using ASF).
Any idea why plugging the USB causes the DMA occasionally stuck in busy state?
Should this problem relates to priorities as I suspect, any idea how to reduce the USB interrupt priority?
code:
void GSWatchMainProcess(void)
{
static int i=0;
GSPacket PacketToStream;
switch (KnockKnockStateMachine)
{
case InitKnockKnock:
KnockKnockStateMachine = KnockKnockStandby;
break;
case KnockKnockStandby:
if (StreamADC && !RequestForAcknowledge) KnockKnockStateMachine = WakeupAphrodite;
KnockKnockStateMachine = WakeupAphrodite; //this line was added to skip waiting for a command from the PC
break;
case WakeupAphrodite:
if (dma_is_busy(&example_resource))
{
KnockKnockStateMachine = AbortKnockKnock;
}
else
{
port_pin_set_output_level(PIN_PB09, true);
port_pin_set_output_level(LED_0_PIN, false);
transfer_is_done = false;
if(dma_start_transfer_job(&example_resource))
{
KnockKnockStateMachine = AbortKnockKnock;
}
}
KnockKnockStateMachine = WaitForBurstToEnd;
i=200000; //this counter is used as work-around to reset the DMA when it get stuck after timeout
break;
case WaitForBurstToEnd:
if (!dma_is_busy(&example_resource))
{
KnockKnockStateMachine = ProcessBurst;
}
if(!--i) // work-around to reset DMA when it get stuck
{
KnockKnockStateMachine = AbortKnockKnock;
}
break;
case ProcessBurst:
PacketToStream.Type=Stream;
PacketToStream.ContentLength=0;
for (i = 0; i<(ADC_SAMPLES); i++)
{
PacketToStream.Content[PacketToStream.ContentLength++] = adc_result_buffer[i] / 256;
PacketToStream.Content[PacketToStream.ContentLength++] = adc_result_buffer[i] % 256;
if(PacketToStream.ContentLength>=PACKET_MAX_SIZE)
{
//SendViaGSWatchLink(PacketToStream);
PacketToStream.ContentLength=0;
}
}
//if(PacketToStream.ContentLength>0) SendViaGSWatchLink(PacketToStream);
RequestForAcknowledge = true;
KnockKnockStateMachine = KnockKnockStandby;
break;
case AbortKnockKnock:
dma_abort_job(&example_resource);
dma_free(&example_resource);
port_pin_set_output_level(PIN_PB09, false);
port_pin_set_output_level(LED_0_PIN, true);
transfer_is_done = true;
configure_dma_resource(&example_resource);
setup_transfer_descriptor(&DMA_ADC_descriptor);
dma_add_descriptor(&example_resource, &DMA_ADC_descriptor);
dma_register_callback(&example_resource, transfer_done, DMA_CALLBACK_TRANSFER_DONE);
dma_enable_callback(&example_resource, DMA_CALLBACK_TRANSFER_DONE);
system_interrupt_enable_global();
KnockKnockStateMachine = WakeupAphrodite;
break;
}
}
void transfer_done(struct dma_resource* const resource )
{
transfer_is_done = true;
port_pin_set_output_level(PIN_PB09, false);
port_pin_set_output_level(LED_0_PIN, true);
}
void setup_transfer_descriptor(DmacDescriptor *descriptor)
{
struct dma_descriptor_config descriptor_config;
dma_descriptor_get_config_defaults(&descriptor_config);
descriptor_config.beat_size = DMA_BEAT_SIZE_HWORD;
descriptor_config.block_transfer_count = ADC_SAMPLES;
descriptor_config.dst_increment_enable = true;
descriptor_config.src_increment_enable = false;
descriptor_config.source_address = (uint32_t)(&adc_instance.hw->RESULT.reg);
//descriptor_config.destination_address = (uint32_t)adc_result_buffer + 2 * ADC_SAMPLES;
descriptor_config.destination_address = (uint32_t)adc_result_buffer + sizeof(adc_result_buffer);
dma_descriptor_create(descriptor, &descriptor_config);
}
void configure_dma_resource(struct dma_resource *resource)
{
struct dma_resource_config config_dma;
dma_get_config_defaults(&config_dma);
config_dma.peripheral_trigger = ADC_DMAC_ID_RESRDY;
config_dma.trigger_action = DMA_TRIGGER_ACTON_BEAT;
config_dma.priority = DMA_PRIORITY_LEVEL_3;
dma_allocate(resource, &config_dma);
}
void configure_adc(void)
{
struct adc_config config_adc;
adc_get_config_defaults(&config_adc);
// config_adc.gain_factor = ADC_GAIN_FACTOR_DIV2; //TODO: check if we need this feature
config_adc.clock_prescaler = ADC_CLOCK_PRESCALER_DIV32; //TODO: check whether it possible to work with 8
config_adc.reference = ADC_REFERENCE_INTVCC0; //ADC_REFERENCE_INT1V; //ADC_REFERENCE_INTVCC1;
config_adc.positive_input = ADC_POSITIVE_INPUT_PIN8;
config_adc.negative_input = ADC_NEGATIVE_INPUT_GND;
config_adc.resolution = ADC_RESOLUTION_12BIT;
config_adc.sample_length = 0; //4
config_adc.differential_mode = false;
config_adc.freerunning = true;
adc_init(&adc_instance, ADC, &config_adc);
adc_enable(&adc_instance);
}
I experienced a very similar issue. My SPI workes fine although I plugged in the USB but started with this Issue when I start it from a specific USB-CDC command.
I am working with asf 3.34 on as 7 and a L21 (very similar).
My workaround which is not clean but works:
After starting the DMA transfer I continiusly(while-loop) check for the transfer done bit(REG_DMAC_CHINTFLAG should be 0x2) and when it is I set the status to ok.
In Code:
transfer_tx_is_done=0;
dma_start_transfer_job(&res_2_Byte);
while (!transfer_tx_is_done) {
/* Wait for transfer done */
if (SPI_done())
{
res_2_Byte.job_status=STATUS_OK;
break;
}
}
where SPI_done() checks the register and transfer_tx_is_done would be set by the interrupt (works sometimes [I said it is dirty])
I am no expert in USB and this is possibly unrelated to you case but I got also issues with USB-CDC and occasional freezing on UC3A3 chips I found out 2 separate reasons:
USB interrupt and tasks must have enough free MCU time
I am setting test bits on entering and exiting of each interrupt I got (USB included) and if they are too close to overlap weird things start to happen like freezing, output signal jitter (way bigger then all ISRs together), sync and acknowledge errors etc even if USB has the highest priority. If I re-time all the stuff I use so the interrupts are firing not at the same times all works good.
beware GPIO toggling is slow operation so you need to take that into account too.
Each version of Host OS (windows) has different timing
I use my MCU for full duplex synchronous bulk transfers and got 3 layers of FIFO (2 on Host and 1 on MCU) to keep up the sync (contnuous 24/7 ~640KByte/s in and ~640KByte/s out). With each new version of Windows (w2k -> Xp --> w7) something changed in either scheduling of threads or driver services and re-timing was necessary changing transfer times and timeouts so the lock less multi threads for RT synchronous USB transfers still work as should.
Latest thing I discover in W7 (they added new "feature") is that some USB controllers (like Intel) on Host side have either freezes on their own or different priorities of data transfers (either based on pipe or direction) and sending/receiving ratio 1:1 no more works on some machines causing freezes on the MCU side from 1ms up to few seconds due to blocked FIFOs. The workaround for that is to fully fill MCU receiving FIFO's (or increase MCU FIFO size which is not possible in my case as it takes almost all memory already) which should be done anyway but in my case I work in RT and do not have many packets ahead so I find out I need to send at least 3 times more packets then receive until the FIFOs full-fill over time (and each time the Host sending freezes which is for some machines all the time) just not to lost sync and all the half full FIFO mechanism on those machines not work anymore.
So in case your USB transfer is synchronized with host or the other way around it is worth to check different OS (like Xp) if the problem is also there. If not there is a high chance you got similar problems I was dealing with so you can try the workarounds ...

FTDI Communication with USB device - Objective C

I'm trying to communicate with the Enttec USB DMX Pro. Mainly receiving DMX.
They released a Visual C++ version here, but I'm a little stumped on what to do to convert to Obj-c. Enttec writes, "Talk to the PRO using FTDI library for Mac, and refer to D2XX programming guide to open and talk to the device." Any example apps for Objective-C out there? Is there an easy way to communicate with the Enttec DMX USB Pro?
I've done a significant amount of work with the FTDI chips on the Mac, so I can provide a little insight here. I've used the single-channel and dual-channel variants of their USB-serial converters, and they all behave the same way.
FTDI has both their Virtual COM Port drivers, which create a serial COM port on your system representing the serial connection attached to their chip, and their D2XX direct communication libraries. You're going to want to work with the latter, which can be downloaded from their site for various platforms.
The D2XX libraries for the Mac come in a standalone .dylib (the latest being libftd2xx.1.2.2.dylib) or a new static library they started shipping recently. Included in that package will be the appropriate header files you need (ftd2xx.h and WinTypes.h) as well.
In your Xcode project, add the .dylib as a framework to be linked in, and add the ftd2xx.h, WinTypes.h, and ftd2xx.cfg files to your project. In your Copy Bundled Frameworks build phase, make sure that libftd2xx.1.2.2.dylib and ftd2xx.cfg are present in that phase. You may also need to adjust the relative path that this library expects, in order for it to function within your app bundle, so you may need to run the following command against it at the command line:
install_name_tool -id #executable_path/../Frameworks/libftd2xx.1.2.2.dylib libftd2xx.1.2.2.dylib
Once your project is all properly configured, you'll want to import the FTDI headers:
#import "ftd2xx.h"
and start to connect to your serial devices. The example you link to in your question has a downloadable C++ sample that shows how they communicate to their device. You can bring across almost all of the C code used there and place it within your Objective-C application. They just look to be using the standard FTDI D2XX commands, which are described in detail within the downloadable D2XX Programmer's Guide.
This is some code that I've lifted from one of my applications, used to connect to one of these devices:
DWORD numDevs = 0;
// Grab the number of attached devices
ftdiPortStatus = FT_ListDevices(&numDevs, NULL, FT_LIST_NUMBER_ONLY);
if (ftdiPortStatus != FT_OK)
{
NSLog(#"Electronics error: Unable to list devices");
return;
}
// Find the device number of the electronics
for (int currentDevice = 0; currentDevice < numDevs; currentDevice++)
{
char Buffer[64];
ftdiPortStatus = FT_ListDevices((PVOID)currentDevice,Buffer,FT_LIST_BY_INDEX|FT_OPEN_BY_DESCRIPTION);
NSString *portDescription = [NSString stringWithCString:Buffer encoding:NSASCIIStringEncoding];
if ( ([portDescription isEqualToString:#"FT232R USB UART"]) && (usbRelayPointer != NULL))
{
// Open the communication with the USB device
ftdiPortStatus = FT_OpenEx("FT232R USB UART",FT_OPEN_BY_DESCRIPTION,usbRelayPointer);
if (ftdiPortStatus != FT_OK)
{
NSLog(#"Electronics error: Can't open USB relay device: %d", (int)ftdiPortStatus);
return;
}
//Turn off bit bang mode
ftdiPortStatus = FT_SetBitMode(*usbRelayPointer, 0x00,0);
if (ftdiPortStatus != FT_OK)
{
NSLog(#"Electronics error: Can't set bit bang mode");
return;
}
// Reset the device
ftdiPortStatus = FT_ResetDevice(*usbRelayPointer);
// Purge transmit and receive buffers
ftdiPortStatus = FT_Purge(*usbRelayPointer, FT_PURGE_RX | FT_PURGE_TX);
// Set the baud rate
ftdiPortStatus = FT_SetBaudRate(*usbRelayPointer, 9600);
// 1 s timeouts on read / write
ftdiPortStatus = FT_SetTimeouts(*usbRelayPointer, 1000, 1000);
// Set to communicate at 8N1
ftdiPortStatus = FT_SetDataCharacteristics(*usbRelayPointer, FT_BITS_8, FT_STOP_BITS_1, FT_PARITY_NONE); // 8N1
// Disable hardware / software flow control
ftdiPortStatus = FT_SetFlowControl(*usbRelayPointer, FT_FLOW_NONE, 0, 0);
// Set the latency of the receive buffer way down (2 ms) to facilitate speedy transmission
ftdiPortStatus = FT_SetLatencyTimer(*usbRelayPointer,2);
if (ftdiPortStatus != FT_OK)
{
NSLog(#"Electronics error: Can't set latency timer");
return;
}
}
}
Disconnection is fairly simple:
ftdiPortStatus = FT_Close(*electronicsPointer);
*electronicsPointer = 0;
if (ftdiPortStatus != FT_OK)
{
return;
}
Writing to the serial device is then pretty easy:
__block DWORD bytesWrittenOrRead;
unsigned char * dataBuffer = (unsigned char *)[command bytes];
//[command getBytes:dataBuffer];
runOnMainQueueWithoutDeadlocking(^{
ftdiPortStatus = FT_Write(electronicsCommPort, dataBuffer, (DWORD)[command length], &bytesWrittenOrRead);
});
if((bytesWrittenOrRead < [command length]) || (ftdiPortStatus != FT_OK))
{
NSLog(#"Bytes written: %d, should be:%d, error: %d", bytesWrittenOrRead, (unsigned int)[command length], ftdiPortStatus);
return NO;
}
(command is an NSData instance, and runOnMainQueueWithoutDeadlocking() is merely a convenience function I use to guarantee execution of a block on the main queue).
You can read raw bytes from the serial interface using something like the following:
NSData *response = nil;
DWORD numberOfCharactersToRead = size;
__block DWORD bytesWrittenOrRead;
__block unsigned char *serialCommunicationBuffer = malloc(numberOfCharactersToRead);
runOnMainQueueWithoutDeadlocking(^{
ftdiPortStatus = FT_Read(electronicsCommPort, serialCommunicationBuffer, (DWORD)numberOfCharactersToRead, &bytesWrittenOrRead);
});
if ((bytesWrittenOrRead < numberOfCharactersToRead) || (ftdiPortStatus != FT_OK))
{
free(serialCommunicationBuffer);
return nil;
}
response = [[NSData alloc] initWithBytes:serialCommunicationBuffer length:numberOfCharactersToRead];
free(serialCommunicationBuffer);
At the end of the above, response will be an NSData instance containing the bytes you've read from the port.
Additionally, I'd suggest that you should always access the FTDI device from the main thread. Even though they say they support multithreaded access, I've found that any kind of non-main-thread access (even guaranteed exclusive accesses from a single thread) cause intermittent crashes on the Mac.
Beyond the cases I've described above, you can consult the D2XX programming guide for the other functions FTDI provides in their C library. Again, you should just need to move over the appropriate code from the samples that have been provided to you by your device manufacturer.
I was running into a similar issue (trying to write to the EntTec Open DMX using Objective-C), without any success. After following #Brad's great answer, I realized that you also need to toggle the BREAK state each time you send a DMX packet.
Here's an example of my loop in some testing code that sends packets with a 20 millisecond delay between frames.
while (1) {
FT_SetBreakOn(usbRelayPointer);
FT_SetBreakOff(usbRelayPointer);
ftdiPortStatus = FT_Write(usbRelayPointer, startCode, 1, &bytesWrittenOrRead);
ftdiPortStatus = FT_Write(usbRelayPointer, dataBuffer, (DWORD)[command length], &bytesWrittenOrRead);
usleep(20000);
}
Hope this helps someone else out there!