co2minimon

Get temperature and CO2 concentration data from a CO2Mini sensor.
git clone git://git.amin.space/co2minimon.git
Log | Files | Refs | README | LICENSE

main.c (9131B)


      1 #include <assert.h>
      2 #include <errno.h>
      3 #include <stdbool.h>
      4 #include <stdio.h>
      5 #include <stdint.h>
      6 
      7 #include <fcntl.h>
      8 #include <signal.h>
      9 #include <sys/ioctl.h>
     10 #include <sys/select.h>
     11 #include <sys/stat.h>
     12 #include <unistd.h>
     13 
     14 // TODO: Use syslog.h for logging
     15 
     16 // https://www.kernel.org/doc/Documentation/hid/hidraw.txt
     17 #include <linux/hidraw.h>
     18 
     19 #define UNREACHABLE assert(false)
     20 
     21 enum State
     22 {
     23     DEVICE_HANDLE_INACCESSIBLE,
     24     DEVICE_HANDLE_ACCESSIBLE,
     25     DEVICE_HANDLE_OPEN,
     26     DEVICE_SENDING_DATA,
     27     FATAL_ERROR,
     28 };
     29 
     30 void print_state(enum State state)
     31 {
     32     switch (state)
     33     {
     34         case DEVICE_HANDLE_INACCESSIBLE:
     35             printf("DEVICE_HANDLE_INACCESSIBLE\n");
     36             break;
     37         case DEVICE_HANDLE_ACCESSIBLE:
     38             printf("DEVICE_HANDLE_ACCESSIBLE\n");
     39             break;
     40         case DEVICE_HANDLE_OPEN:
     41             printf("DEVICE_HANDLE_OPEN\n");
     42             break;
     43         case DEVICE_SENDING_DATA:
     44             printf("DEVICE_SENDING_DATA\n");
     45             break;
     46         case FATAL_ERROR:
     47             printf("FATAL_ERROR\n");
     48             break;
     49         default:
     50             assert(false);
     51             break;
     52     }
     53 }
     54 
     55 volatile static bool running = true;
     56 
     57 static void stop_running(int signal)
     58 {
     59     (void)signal;
     60     running = false;
     61 }
     62 
     63 // Made possible by Henryk Plötz' excellent work:
     64 // https://hackaday.io/project/5301-reverse-engineering-a-low-cost-usb-co-monitor
     65 int main(void)
     66 {
     67     {
     68         struct sigaction act =
     69         {
     70             .sa_handler = stop_running,
     71         };
     72         int failure = sigaction(SIGINT, &act, NULL);
     73         failure |= sigaction(SIGTERM, &act, NULL);
     74         if (failure)
     75         {
     76             printf("ERROR: Failed to register signal handler.\n");
     77             return 1;
     78         }
     79     }
     80 
     81     // NOTE: Included udev rules create this symlink to the appropriate hidraw
     82     // entry.
     83     const char *hid_file_path         = "/dev/co2mini0";
     84     const char *temperature_file_path = "/tmp/co2minimon_temp";
     85     const char *co2_file_path         = "/tmp/co2minimon_co2";
     86 
     87     enum State state = DEVICE_HANDLE_INACCESSIBLE;
     88     int device_handle = -1;
     89 
     90     while (running)
     91     {
     92         switch (state)
     93         {
     94             case DEVICE_HANDLE_INACCESSIBLE:
     95             {
     96                 if (access(hid_file_path, F_OK) == 0)
     97                 {
     98                     state = DEVICE_HANDLE_ACCESSIBLE;
     99                 }
    100                 else
    101                 {
    102                     sleep(30);
    103                 }
    104             }
    105             break; case DEVICE_HANDLE_ACCESSIBLE:
    106             {
    107                 errno = 0;
    108                 device_handle = open(hid_file_path, O_RDWR);
    109                 if (device_handle >= 0)
    110                 {
    111                     state = DEVICE_HANDLE_OPEN;
    112                 }
    113                 else if (errno == ENOENT)
    114                 {
    115                     state = DEVICE_HANDLE_INACCESSIBLE;
    116                 }
    117                 else if (errno != EINTR)
    118                 {
    119                     printf("ERROR: Failed to open HID.\n");
    120                     state = FATAL_ERROR;
    121                 }
    122             }
    123             break; case DEVICE_HANDLE_OPEN:
    124             {
    125                 uint8_t key[8] = {0};
    126                 errno = 0;
    127                 int result = ioctl(device_handle, HIDIOCSFEATURE(sizeof(key)), key);
    128                 if (result == sizeof(key))
    129                 {
    130                     state = DEVICE_SENDING_DATA;
    131                 }
    132                 else if (result < 0)
    133                 {
    134                     if (errno == ENODEV)
    135                     {
    136                         // TODO: do we need to do this in any other cases?
    137                         close(device_handle);
    138                         unlink(co2_file_path);
    139                         unlink(temperature_file_path);
    140                         state = DEVICE_HANDLE_ACCESSIBLE;
    141                     }
    142                     else if (errno != EINTR)
    143                     {
    144                         printf("ERROR: Failed to send feature report.\n");
    145                         state = FATAL_ERROR;
    146                     }
    147                 }
    148                 else
    149                 {
    150                     // TODO: when does this happen
    151                     printf("ERROR: Failed to send feature report.\n");
    152                     state = FATAL_ERROR;
    153                 }
    154             }
    155             break; case DEVICE_SENDING_DATA:
    156             {
    157                 fd_set read_fds;
    158                 FD_ZERO(&read_fds);
    159                 FD_SET(device_handle, &read_fds);
    160                 errno = 0;
    161                 int ready = select(device_handle + 1, &read_fds, NULL, NULL, &(struct timeval){ .tv_sec = 15 });
    162                 if (ready == -1)
    163                 {
    164                     if (errno != EINTR)
    165                     {
    166                         printf("ERROR: Failed to select() device handle.\n");
    167                         state = FATAL_ERROR;
    168                     }
    169                 }
    170                 else if (ready == 0)
    171                 {
    172                     // select() timed out
    173                     //
    174                     // If we go too long with an empty pipe, the device may have
    175                     // stopped sending data. One situation where I've observed this can
    176                     // happen is when the system resumes from hibernation. In this
    177                     // case, all calls to read() will block until we resend the feature
    178                     // report.
    179                     //
    180                     // I'm not sure why this happens, but my guess would be that the
    181                     // device stops sending new data if the data hasn't been read in a
    182                     // while.
    183                     state = DEVICE_HANDLE_OPEN;
    184                     //TODO: why do we ooscillate?
    185                 }
    186                 else
    187                 {
    188                     uint8_t data[8] = {0};
    189                     errno = 0;
    190                     int bytes_read = read(device_handle, &data, sizeof(data));
    191                     if (bytes_read < 0 && errno != EINTR)
    192                     {
    193                         if (errno == EIO)
    194                         {
    195                             state = DEVICE_HANDLE_OPEN;
    196                             break;
    197                         }
    198                         else
    199                         {
    200                             printf("ERROR: Failed to read device handle\n");
    201                             state = FATAL_ERROR;
    202                             break;
    203                         }
    204                     }
    205 
    206                     uint8_t item     = data[0];
    207                     uint8_t msb      = data[1];
    208                     uint8_t lsb      = data[2];
    209                     uint8_t checksum = data[3];
    210                     uint8_t end      = data[4];
    211 
    212                     if (bytes_read == sizeof(data)
    213                         && end == 0x0d
    214                         && (item == 0x42 || item == 0x50)
    215                         && ((item + msb + lsb) & 0xFF) == checksum)
    216                     {
    217                         uint16_t value = (((uint16_t)msb) << 8) | lsb;
    218                         char buf[1024] = {0};
    219                         int str_len = 0;
    220                         mode_t create_mode = S_IRUSR | S_IWUSR;
    221                         int open_mode = O_WRONLY | O_CREAT | O_TRUNC;
    222 
    223                         switch(item)
    224                         {
    225                             case 0x42:
    226                             {
    227                                 double t_celsius = value / 16.0 - 273.15;
    228                                 str_len = snprintf(buf, sizeof(buf), "%.2f", t_celsius);
    229                                 assert(str_len > 0);
    230                                 int f = open(temperature_file_path, open_mode, create_mode);
    231                                 if (f == -1)
    232                                 {
    233                                     printf("ERROR: Failed to open output file for temperature.\n");
    234                                     state = FATAL_ERROR;
    235                                 }
    236                                 write(f, buf, str_len);
    237                                 close(f);
    238                             } break;
    239 
    240                             case 0x50:
    241                             {
    242                                 str_len = snprintf(buf, sizeof(buf), "%d", value);
    243                                 assert(str_len > 0);
    244                                 int f = open(co2_file_path, open_mode, create_mode);
    245                                 if (f == -1)
    246                                 {
    247                                     printf("ERROR: Failed to open output file for CO2.\n");
    248                                     state = FATAL_ERROR;
    249                                 }
    250                                 write(f, buf, str_len);
    251                                 close(f);
    252                             } break;
    253                         }
    254                     }
    255                 }
    256             }
    257             break; case FATAL_ERROR:
    258                 running = false;
    259             break; default:
    260                 UNREACHABLE;
    261             break;
    262         }
    263     }
    264 
    265     unlink(co2_file_path);
    266     unlink(temperature_file_path);
    267     close(device_handle);
    268 
    269     return state == FATAL_ERROR;
    270 }