// // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2017 John Langner, WB2OSZ // // Parts of this were adapted from "hamlib" which contains the notice: // // * Copyright (c) 2000-2012 by Stephane Fillod // * Copyright (c) 2011 by Andrew Errington // * CM108 detection code Copyright (c) Thomas Sailer used with permission // // 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 2 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 . /*------------------------------------------------------------------ * * Module: cm108.c * * Purpose: Use the CM108/CM119 (or compatible) GPIO pins for the Push To Talk (PTT) Control. * * Description: * * There is an incresing demand for using the GPIO pins of USB audio devices for PTT. * We have a couple commercial products: * * DMK URI http://www.dmkeng.com/URI_Order_Page.htm * RB-USB RIM http://www.repeater-builder.com/products/usb-rim-lite.html * * and homebrew projects which are all very similar. * * http://www.qsl.net/kb9mwr/projects/voip/usbfob-119.pdf * http://rtpdir.weebly.com/uploads/1/6/8/7/1687703/usbfob.pdf * http://www.repeater-builder.com/projects/fob/USB-Fob-Construction.pdf * https://irongarment.wordpress.com/2011/03/29/cm108-compatible-chips-with-gpio/ * * Usually GPIO 3 is used because it is easier to tack solder a wire to a pin on the end. * * Soundmodem and hamlib paved the way but didn't get too far. * Dire Wolf 1.3 added HAMLIB support (Linux only) which theoretically allows this in a * roundabout way. This is documented in the User Guide, section called, * "Hamlib PTT Example 2: Use GPIO of USB audio adapter. (e.g. DMK URI)" * * It's rather involved and the explantion doesn't cover the case of multiple * USB-Audio adapters. It is not as straightforward as you might expect. Here we have * an example of 3 C-Media USB adapters, a SignaLink USB, a keyboard, and a mouse. * * * VID PID Product Sound ADEVICE HID [ptt] * --- --- ------- ----- ------- --------- * ** 0d8c 000c C-Media USB Headphone Set /dev/snd/pcmC1D0c plughw:1,0 /dev/hidraw0 * ** 0d8c 000c C-Media USB Headphone Set /dev/snd/pcmC1D0p plughw:1,0 /dev/hidraw0 * ** 0d8c 000c C-Media USB Headphone Set /dev/snd/controlC1 /dev/hidraw0 * 08bb 2904 USB Audio CODEC /dev/snd/pcmC2D0c plughw:2,0 /dev/hidraw2 * 08bb 2904 USB Audio CODEC /dev/snd/pcmC2D0p plughw:2,0 /dev/hidraw2 * 08bb 2904 USB Audio CODEC /dev/snd/controlC2 /dev/hidraw2 * ** 0d8c 000c C-Media USB Headphone Set /dev/snd/pcmC0D0c plughw:0,0 /dev/hidraw1 * ** 0d8c 000c C-Media USB Headphone Set /dev/snd/pcmC0D0p plughw:0,0 /dev/hidraw1 * ** 0d8c 000c C-Media USB Headphone Set /dev/snd/controlC0 /dev/hidraw1 * ** 0d8c 0008 C-Media USB Audio Device /dev/snd/pcmC4D0c plughw:4,0 /dev/hidraw6 * ** 0d8c 0008 C-Media USB Audio Device /dev/snd/pcmC4D0p plughw:4,0 /dev/hidraw6 * ** 0d8c 0008 C-Media USB Audio Device /dev/snd/controlC4 /dev/hidraw6 * 413c 2010 Dell USB Keyboard /dev/hidraw4 * 0461 4d15 USB Optical Mouse /dev/hidraw5 * * * The USB soundcards (/dev/snd/pcm...) have an associated Human Interface Device (HID) * corresponding to the GPIO pins which are sometimes connected to pushbuttons. * The mapping has no obvious pattern. * * Sound Card 0 HID 1 * Sound Card 1 HID 0 * Sound Card 2 HID 2 * Sound Card 4 HID 6 * * That would be a real challenge if you had to figure that all out and configure manually. * Dire Wolf version 1.5 makes this much more flexible and easier to use by supporting multiple * sound devices and automatically determining the corresponding HID for the PTT signal. * *---------------------------------------------------------------*/ #ifndef USE_CM108 #ifdef CM108_MAIN #include "direwolf.h" #include "textcolor.h" int main (void) { text_color_init (0); // Turn off text color. dw_printf ("CM108 PTT support was disabled in Makefile.linux.\n"); return (0); } #endif #else // USE_CM108 is defined. #include "direwolf.h" #include #include #include #include #include #include #include #include #include #include // ioctl, _IOR #include #include #include // for HIDIOCGRAWINFO #include "textcolor.h" #include "cm108.h" static int cm108_write (char *name, int iomask, int iodata); // The CM108, CM109, and CM119 datasheets all say that idProduct can be in the range // of 0008 to 000f programmable by MSEL and MODE pin. How can we tell the difference? // CM108B is 0012. // CM119B is 0013. // CM108AH is 0139 programmable by MSEL and MODE pin. // CM119A is 013A programmable by MSEL and MODE pin. // To make matters even more confusing, these can be overridden // with an external EEPROM. Some have 8, rather than 4 GPIO. #define CMEDIA_VID 0xd8c // Vendor ID #define CMEDIA_PID1_MIN 0x0008 // range for CM108, CM109, CM119 (no following letters) #define CMEDIA_PID1_MAX 0x000f #define CMEDIA_PID2 0x013a // CM119A #define CMEDIA_PID_CM108AH 0x0139 // CM108AH #define CMEDIA_PID_CM108B 0x0012 // CM108B #define CMEDIA_PID_CM119A 0x013a // CM119A #define CMEDIA_PID_CM119B 0x0013 // CM119B #define CMEDIA_PID_HS100 0x013c // HS100 // The SSS chips seem to be pretty much compatible but they have only two GPIO. // https://irongarment.wordpress.com/2011/03/29/cm108-compatible-chips-with-gpio/ // Data sheet says VID/PID is from an EEPROM but mentions no default. #define SSS_VID 0x0c76 // SSS1621, SSS1623 #define SSS_PID1 0x1605 #define SSS_PID2 0x1607 #define SSS_PID3 0x160b // Device VID PID Number of GPIO // ------ --- --- -------------- // CM108 0d8c 0008-000f * 4 // CM108AH 0d8c 0139 * 3 Has GPIO 1,3,4 but not 2 // CM108B 0d8c 0012 3 Has GPIO 1,3,4 but not 2 // CM109 0d8c 0008-000f * 8 // CM119 0d8c 0008-000f * 8 // CM119A 0d8c 013a * 8 // CM119B 0d8c 0013 8 // HS100 0d8c 013c 0 // // SSS1621 0c76 1605 2 per ZL3AME, Can't find data sheet // SSS1623 0c76 1607,160b 2 per ZL3AME, Not in data sheet. // // * idProduct programmable by MSEL and MODE pin. // // CMedia pin GPIO Notes // ---------- ---- ----- // 43 1 // 11 2 N.C. for CM108AH, CM108B // 13 3 Most popular for PTT because it is on the end. // 15 4 // 16 5 CM109, CM119, CM119A, CM119B only // 17 6 " // 20 7 " // 22 8 " // Test for supported devices. #define GOOD_DEVICE(v,p) ( (v == CMEDIA_VID && ((p >= CMEDIA_PID1_MIN && p <= CMEDIA_PID1_MAX) || p == CMEDIA_PID2)) || \ (v == SSS_VID && (p == SSS_PID1 || p == SSS_PID2 || p == SSS_PID3)) ) // Look out for null source pointer, and avoid buffer overflow on destination. #define SAFE_STRCPY(to,from) { if (from != NULL) { strncpy(to,from,sizeof(to)); to[sizeof(to)-1] = '\0'; } } // Used to process regular expression matching results. static void inline substr_se (char *dest, const char *src, int start, int endp1) { int len = endp1 - start; if (start < 0 || endp1 < 0 || len <= 0) { dest[0] = '\0'; return; } memcpy (dest, src + start, len); dest[len] = '\0'; } /* end substr_se */ /* * Result of taking inventory of USB soundcards and USB HIDs. */ struct thing_s { int vid; // vendor id int pid; // product id char product[32]; // product name (e.g. manufacturer, model) char devnode_sound[22]; // e.g. /dev/snd/pcmC0D0p char plughw[15]; // Above in more familiar format e.g. plughw:0,0 char devnode_hidraw[17]; // e.g. /dev/hidraw3 char devnode_usb[25]; // e.g. /dev/bus/usb/001/012 }; int cm108_inventory (struct thing_s *things, int max_things); /*------------------------------------------------------------------- * * Name: main * * Purpose: Test program to list USB audio and HID devices. * * sudo apt-get install libudev-dev * gcc -DCM108_MAIN textcolor.c -l udev * *------------------------------------------------------------------*/ #define MAXX_THINGS 60 #ifdef CM108_MAIN int main (void) { struct thing_s things[MAXX_THINGS]; int num_things; int i; text_color_init (0); // Turn off text color. num_things = cm108_inventory (things, MAXX_THINGS); dw_printf (" VID PID %-*s %-*s %-*s %-*s" #if EXTRA " %-*s" #endif "\n", (int)sizeof(things[0].product), "Product", (int)sizeof(things[0].devnode_sound), "Sound", (int)sizeof(things[0].plughw), "ADEVICE", (int)sizeof(things[0].devnode_hidraw), "HID [ptt]" #if EXTRA , (int)sizeof(things[0].devnode_usb), "USB" #endif ); dw_printf (" --- --- %-*s %-*s %-*s %-*s" #if EXTRA " %-*s" #endif "\n", (int)sizeof(things[0].product), "-------", (int)sizeof(things[0].devnode_sound), "-----", (int)sizeof(things[0].plughw), "-------", (int)sizeof(things[0].devnode_hidraw), "---------" #if EXTRA , (int)sizeof(things[0].devnode_usb), "---" #endif ); for (i = 0; i < num_things; i++) { dw_printf ("%2s %04x %04x %-*s %-*s %-*s %-*s" #if EXTRA " %-*s" #endif "\n", GOOD_DEVICE(things[i].vid,things[i].pid) ? "**" : " ", things[i].vid, things[i].pid, (int)sizeof(things[i].product), things[i].product, (int)sizeof(things[i].devnode_sound), things[i].devnode_sound, (int)sizeof(things[0].plughw), things[i].plughw, (int)sizeof(things[i].devnode_hidraw), things[i].devnode_hidraw #if EXTRA , (int)sizeof(things[i].devnode_usb), things[i].devnode_usb #endif ); } return (0); } #endif // CM108_MAIN /*------------------------------------------------------------------- * * Name: cm108_inventory * * Purpose: Take inventory of USB audio and HID. * * Inputs: max_things - Maximum number of items to collect. * * Outputs: things - Array of items collected. * Corresponding sound device and HID are merged into one item. * * Returns: Number of items placed in things array. * Should be in the range of 0 thru max_things. * -1 for a bad unexpected error. * *------------------------------------------------------------------*/ int cm108_inventory (struct thing_s *things, int max_things) { struct udev *udev; struct udev_enumerate *enumerate; struct udev_list_entry *devices, *dev_list_entry; struct udev_device *dev; struct udev_device *parentdev; int num_things = 0; memset (things, 0, sizeof(struct thing_s) * max_things); /* * First get a list of the USB audio devices. * This is based on the example in http://www.signal11.us/oss/udev/ */ udev = udev_new(); if (!udev) { text_color_set(DW_COLOR_ERROR); dw_printf("INTERNAL ERROR: Can't create udev.\n"); return (-1); } enumerate = udev_enumerate_new(udev); udev_enumerate_add_match_subsystem(enumerate, "sound"); udev_enumerate_scan_devices(enumerate); devices = udev_enumerate_get_list_entry(enumerate); udev_list_entry_foreach(dev_list_entry, devices) { const char *path; path = udev_list_entry_get_name(dev_list_entry); dev = udev_device_new_from_syspath(udev, path); char const *devnode = udev_device_get_devnode(dev); if (devnode != NULL) { parentdev = udev_device_get_parent_with_subsystem_devtype( dev, "usb", "usb_device"); if (parentdev != NULL) { char const *p; int vid = 0; int pid = 0; p = udev_device_get_sysattr_value(parentdev,"idVendor"); if (p != NULL) vid = strtol(p, NULL, 16); p = udev_device_get_sysattr_value(parentdev,"idProduct"); if (p != NULL) pid = strtol(p, NULL, 16); if (num_things < max_things) { things[num_things].vid = vid; things[num_things].pid = pid; SAFE_STRCPY (things[num_things].product, udev_device_get_sysattr_value(parentdev,"product")); SAFE_STRCPY (things[num_things].devnode_sound, devnode); SAFE_STRCPY (things[num_things].devnode_usb, udev_device_get_devnode(parentdev)); num_things++; } udev_device_unref(parentdev); } } } udev_enumerate_unref(enumerate); udev_unref(udev); /* * Now merge in all of the USB HID. */ udev = udev_new(); if (!udev) { text_color_set(DW_COLOR_ERROR); dw_printf("INTERNAL ERROR: Can't create udev.\n"); return (-1); } enumerate = udev_enumerate_new(udev); udev_enumerate_add_match_subsystem(enumerate, "hidraw"); udev_enumerate_scan_devices(enumerate); devices = udev_enumerate_get_list_entry(enumerate); udev_list_entry_foreach(dev_list_entry, devices) { const char *path; path = udev_list_entry_get_name(dev_list_entry); dev = udev_device_new_from_syspath(udev, path); char const *devnode = udev_device_get_devnode(dev); if (devnode != NULL) { parentdev = udev_device_get_parent_with_subsystem_devtype( dev, "usb", "usb_device"); if (parentdev != NULL) { char const *p; int vid = 0; int pid = 0; p = udev_device_get_sysattr_value(parentdev,"idVendor"); if (p != NULL) vid = strtol(p, NULL, 16); p = udev_device_get_sysattr_value(parentdev,"idProduct"); if (p != NULL) pid = strtol(p, NULL, 16); int j, matched = 0; char const *usb = udev_device_get_devnode(parentdev); // Add hidraw name to any matching existing. for (j = 0; j < num_things; j++) { if (things[j].vid == vid && things[j].pid == pid && usb != NULL && strcmp(things[j].devnode_usb,usb) == 0) { matched = 1; SAFE_STRCPY (things[j].devnode_hidraw, devnode); } } // If it did not match to existing, add new entry. if (matched == 0 && num_things < max_things) { things[num_things].vid = vid; things[num_things].pid = pid; SAFE_STRCPY (things[num_things].product, udev_device_get_sysattr_value(parentdev,"product")); SAFE_STRCPY (things[num_things].devnode_hidraw, devnode); SAFE_STRCPY (things[num_things].devnode_usb, usb); num_things++; } udev_device_unref(parentdev); } } } udev_enumerate_unref(enumerate); udev_unref(udev); /* * Seeing the form /dev/snd/pcmC4D0p will be confusing to many because we * would generally something like plughw:4,0 for in the direwolf configuration file. * Construct the more familiar form. */ int i; regex_t pcm_re; char emsg[100]; int e = regcomp (&pcm_re, "pcmC([0-9]+)D([0-9]+)[cp]", REG_EXTENDED); if (e) { regerror (e, &pcm_re, emsg, sizeof(emsg)); text_color_set(DW_COLOR_ERROR); dw_printf("INTERNAL ERROR: %s:%d: %s\n", __FILE__, __LINE__, emsg); return (-1); } for (i = 0; i < num_things; i++) { regmatch_t match[3]; if (regexec (&pcm_re, things[i].devnode_sound, 3, match, 0) == 0) { char c[32], d[32]; substr_se (c, things[i].devnode_sound, match[1].rm_so, match[1].rm_eo); substr_se (d, things[i].devnode_sound, match[2].rm_so, match[2].rm_eo); snprintf (things[i].plughw, sizeof(things[i].plughw), "plughw:%s,%s", c, d); } } return (num_things); } /* end cm108_inventory */ /*------------------------------------------------------------------- * * Name: cm108_find_ptt * * Purpose: Try to find /dev/hidraw corresponding to plughw:n,n * * Inputs: output_audio_device * - Device name used in the ADEVICE configuration. * This would generally be something like plughw:1,0 * * ptt_device_size - Size of result area to avoid buffer overflow. * * Outputs: ptt_device - Device name, something like /dev/hidraw2. * Will be emptry string if no match found. * * Returns: none * *------------------------------------------------------------------*/ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_device_size) { struct thing_s things[MAXX_THINGS]; int num_things; int i; strlcpy (ptt_device, "", ptt_device_size); num_things = cm108_inventory (things, MAXX_THINGS); for (i = 0; i < num_things; i++) { if (GOOD_DEVICE(things[i].vid,things[i].pid) ) { if (strcmp(output_audio_device, things[i].plughw) == 0) { strlcpy (ptt_device, things[i].devnode_hidraw, ptt_device_size); } } } } /* end cm108_find_ptt */ /*------------------------------------------------------------------- * * Name: cm108_set_gpio_pin * * Purpose: Set one GPIO pin of the CM108 or similar. * * Inputs: name - Name of device such as /dev/hidraw2. * * num - GPIO number, range 1 thru 8. * * state - 1 for on, 0 for off. * * Returns: 0 for success. -1 for error. * * Errors: A descriptive error message will be printed for any problem. * * Future: For our initial implementation we are making the simplifying * restriction of using only one GPIO pin per device and limit * configuratin to PTT only. * Longer term, we might want to have DCD, and maybe other * controls thru the same chip. * In this case, we would need to retain bit masks for each * device so new data can be merged with old before sending it out. * *------------------------------------------------------------------*/ int cm108_set_gpio_pin (char *name, int num, int state) { int iomask; int iodata; if (num < 1 || num > 8) { text_color_set(DW_COLOR_ERROR); dw_printf("%s CM108 GPIO number %d must be in range of 1 thru 8.\n", name, num); return (-1); } if (state != 0 && state != 1) { text_color_set(DW_COLOR_ERROR); dw_printf("%s CM108 GPIO state %d must be 0 or 1.\n", name, state); return (-1); } iomask = 1 << (num - 1); iodata = state << (num - 1); return (cm108_write (name, iomask, iodata)); } /* end cm108_set_gpio_pin */ /*------------------------------------------------------------------- * * Name: cm108_write * * Purpose: Set the GPIO pins of the CM108 or similar. * * Inputs: name - Name of device such as /dev/hidraw2. * * iomask - Bit mask for I/O direction. * LSB is GPIO1, bit 1 is GPIO2, etc. * 1 for output, 0 for input. * * iodata - Output data, same bit order as iomask. * * Returns: 0 for success. -1 for error. * * Errors: A descriptive error message will be printed for any problem. * * Description: This is the lowest level function. * An application probably wants to use cm108_set_gpio_pin. * *------------------------------------------------------------------*/ static int cm108_write (char *name, int iomask, int iodata) { int fd; struct hidraw_devinfo info; char io[5]; int n; //text_color_set(DW_COLOR_DEBUG); //dw_printf ("TEMP DEBUG cm108_write: %s %d %d\n", name, iomask, iodata); /* * By default, the USB HID are accessible only by root: * * crw------- 1 root root 249, 1 ... /dev/hidraw1 * * How should we handle this? * Manually changing it will revert back on the next reboot or * when the device is removed and reinserted. * * According to various articles on the Internet, we should be able to * add a file to /etc/udev/rules.d. "99-direwolf-cmedia.rules" would be a * suitable name. The leading number is the order. We want this to be * near the end. I think the file extension must be ".rules." * * We could completely open it up to everyone like this: * * # Allow ordinary user to access CMedia GPIO for PTT. * SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", MODE="0666" * * Whenever we have CMedia USB audio adapter, it should be accessible by everyone. * This would not apply to other /dev/hidraw* corresponding to keyboard, mouse, etc. * * Notice the == (double =) for testing and := for setting a property. * * If you are concerned about security, you could restrict access to * a particular group, something like this: * * SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", GROUP="audio", MODE="0660" * * I figure "audio" makes more sense than "gpio" because we need to be part of * audio group to use the USB Audio adapter for sound. */ fd = open (name, O_WRONLY); if (fd == -1) { text_color_set(DW_COLOR_ERROR); dw_printf ("Could not open %s for write, errno=%d\n", name, errno); if (errno == EACCES) { // 13 dw_printf ("Type \"ls -l %s\" and verify that it has audio group rw similar to this:\n", name); dw_printf (" crw-rw---- 1 root audio 247, 0 Oct 6 19:24 %s\n", name); dw_printf ("rather than root-only access like this:\n"); dw_printf (" crw------- 1 root root 247, 0 Sep 24 09:40 %s\n", name); } return (-1); } // Just for fun, let's get the device information. #if 1 n = ioctl(fd, HIDIOCGRAWINFO, &info); if (n == 0) { if ( ! GOOD_DEVICE(info.vendor, info.product)) { text_color_set(DW_COLOR_ERROR); dw_printf ("ioctl HIDIOCGRAWINFO failed for %s. errno = %d.\n", name, errno); } } else { text_color_set(DW_COLOR_ERROR); dw_printf ("%s is not a supported device type. Proceed at your own risk. vid=%04x pid=%04x\n", name, info.vendor, info.product); } #endif // To make a long story short, I think we need 0 for the first two bytes. io[0] = 0; io[1] = 0; io[2] = iomask; io[3] = iodata; io[4] = 0; // Writing 4 bytes fails with errno 32, EPIPE, "broken pipe." // Hamlib writes 5 bytes which I don't understand. // Writing 5 bytes works. // I have no idea why. From the CMedia datasheet it looks like we need 4. n = write (fd, io, sizeof(io)); if (n != sizeof(io)) { // Errors observed during development. // as pi EACCES 13 /* Permission denied */ // as root EPIPE 32 /* Broken pipe - Happens if we send 4 bytes */ text_color_set(DW_COLOR_ERROR); dw_printf ("Write to %s failed, n=%d, errno=%d\n", name, n, errno); if (errno == EACCES) { dw_printf ("Type \"ls -l %s\" and verify that it has audio group rw similar to this:\n", name); dw_printf (" crw-rw---- 1 root audio 247, 0 Oct 6 19:24 %s\n", name); dw_printf ("rather than root-only access like this:\n"); dw_printf (" crw------- 1 root root 247, 0 Sep 24 09:40 %s\n", name); } close (fd); return (-1); } close (fd); return (0); } /* end cm108_write */ #endif // ifdef USE_CM108 /* end cm108.c */