[GO] Let's develop something close to embedded with TDD ~ Intermediate review ~

Introduction

Using googletest / googlemock, I am developing software that runs on embedded Linux with TDD. Since the procedure actually developed with TDD is written as it is in real time, there is a random part. I hope you enjoy the process as well. If you have any questions or mistakes, please comment. It will be encouraging.

If you would like to know more about the history so far, please see the previous articles. Try to develop something close to embedded with TDD ~ Preparation ~ Try to develop something close to embedded with TDD-Problem raising- Develop something close to embedded with TDD ~ file open ~ Try to develop something close to embedded with TDD ~ libevdev initialization / termination processing ~ Try to develop something close to embedded with TDD ~ Key input detection version ~

This article may not be useful. The next article will be interesting.

LED control

As I announced last time, I made the LED control behind the scenes. Since there was nothing special to mention, only the results will be posted. Of course I implemented it in TDD. I skipped a little and did not handle the error of LED writing. Don't imitate a good boy! Again, the implementation is such that the internal structure is not visible to the user of this LED control module. It also supports multiple instances. I think that there are many cases where multiple LEDs are controlled, so if you make it like this, you will scale it later.

Test code

Product code


typedef struct LedDriverStruct {
  int fd;
  LedStatus status;
} LedDriverStruct;

LedDriver CreateLedDriver() {
  LedDriver led = calloc(1, sizeof(LedDriverStruct));
  led->fd = -1;
  led->status = LED_UNKNOWN;

  return led;
}

int InitLedDriver(LedDriver self, const char* device_file) {
  self->fd = IO_OPEN(device_file, O_WRONLY|O_NONBLOCK);
  self->status = LED_TURN_OFF;
  if (self->fd < 0) {
    return LED_DRIVER_INIT_ERROR;
  }

  return LED_DRIVER_SUCCESS;
}

void TurnOnLed(LedDriver self) {
  if (self == NULL) return;
  self->status = LED_TURN_ON;
  IO_WRITE(self->fd, "1\n", 2);
}

void TurnOffLed(LedDriver self) {
  if (self == NULL) return;
  self->status = LED_TURN_OFF;
  IO_WRITE(self->fd, "0\n", 2);
}

void ToggleLed(LedDriver self) {
  if (self == NULL || self->status == LED_UNKNOWN) return;

  if (self->status == LED_TURN_OFF) {
    TurnOnLed(self);
  } else {
    TurnOffLed(self);
  }
}

int CleanupLedDriver(LedDriver self) {
  if (self == NULL) return LED_DRIVER_CLEANUP_ERROR;

  int rc = IO_CLOSE(self->fd);
  if (rc < 0) {
    return LED_DRIVER_CLEANUP_ERROR;
  }
  return LED_DRIVER_SUCCESS;
}

void DestroyLedDriver(LedDriver self) {
  if (self == NULL) return;

  free(self);
  self = NULL;
}

Code created in Problem raising

Now, what do you think of this code after a long time?

main.c


#define KEYBOARD_DEVICE "/dev/input/event2"
#define LED_DEVICE      "/sys/class/leds/input2::capslock/brightness"

#define KEY_RELEASED 0
#define KEY_PRESSED 1

static void mainloop() {
  struct libevdev *dev = NULL;
  int key_fd = open(KEYBOARD_DEVICE, O_RDONLY|O_NONBLOCK);
  int rc = libevdev_new_from_fd(key_fd, &dev);

  if (rc < 0) {
    fprintf(stderr, "Failed to init libevdev (%s)\n", strerror(-rc));
    exit(1);
  }

  int led_fd = open(LED_DEVICE, O_WRONLY|O_NONBLOCK);
  if (led_fd < 0) {
    fprintf(stderr, "Failed to init LED device.\n");
    exit(1);
  }

  bool led_on = false;
  do {
    struct input_event ev;
    rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
    if (rc == 0) {
      if (ev.type == EV_KEY && ev.code == KEY_A && ev.value == KEY_PRESSED) {
        led_on = !led_on;
        char buf[2];
        snprintf(buf, 2, "%d", led_on ? 1 : 0);
        write(led_fd, buf, 2);
      }
    }
  } while (rc == 1 || rc == 0 || rc == -EAGAIN);

  libevdev_free(dev);
  close(key_fd);
  close(led_fd);
}

int main() {
  mainloop();
  return 0;
}

This is a code of this size, but how do you test this code? It seems that you have to manually test the entire program. (If you do your best, you can write a unit test ...)

How can we guarantee that degreasing isn't happening when we add new features? In the absence of testing, you don't even know if you're moving forward or back.

When you want to refactor, can you take the plunge? The courage to refactor is provided by testing. Code that hasn't been tested can't be refactored, so it gradually rots.

What if your library is unstable? This time, I'm using a stable library called libevdev. If the library used is unstable, it can be said that the range of influence of the library change is the entire program (to be exact, within the mainloop function).

For example, what if you connect another keyboard and now want to control the NumLock LED with the "B" key? When it looks like the one below, there is already a lot of rotten odor.

static void mainloop() {
  struct libevdev *dev1 = NULL;
  struct libevdev *dev2 = NULL;
  int key_fd1 = open(KEYBOARD_DEVICE1, O_RDONLY|O_NONBLOCK);
  int key_fd2 = open(KEYBOARD_DEVICE2, O_RDONLY|O_NONBLOCK);
  int rc = libevdev_new_from_fd(key_fd1, &dev1);
  int rc2 = libevdev_new_from_fd(key_fd2, &dev2);
//I can't stand writing any more
}

If you're a little more rational, you might use arrays. (The code is fine. I haven't tried compiling)

typedef struct LedStruct {
  struct libevdev *dev;
  int fd;
  bool led_on;
}

static void mainloop() {
  LedStruct leds[2];
  leds[0].fd = open(KEYBOARD_DEVICE1, O_RDONLY|O_NONBLOCK);
  leds[1].fd = open(KEYBOARD_DEVICE2, O_RDONLY|O_NONBLOCK);
...

  do {
    for (int i = 0; i < 2; i++) {
      struct input_event ev;
      rc = libevdev_next_event(leds[i].dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
      if (rc == 0) {
        if (ev.type == target_keys[i] && ev.code == target_codes[i] && ev.value == target_values[i]) {
          leds[i].led_on = !leds[i].led_on;
          char buf[2];
          snprintf(buf, 2, "%d", leds[i].led_on ? 1 : 0);
          write(leds[i].fd, buf, 2);
        }
      }
    }
  } while (rc == 1 || rc == 0 || rc == -EAGAIN);
}

It's going to be a lot easier. In the while conditional expression, you only check the return code when i is 1 (did you notice?).

The fact that the concrete (device operation) is solidly written and hinders abstraction is also a major obstacle to expanding the software.

Code made with TDD

I have made good use of unit tests in TDD to guide good designs. Let's take a look at the code we have created so far.

key_input_event.c


typedef struct KeyInputDeviceStruct {
  int fd;
  struct libevdev *evdev;
  struct input_event target_event;
} KeyInputDeviceStruct;

KeyInputDevice CreateKeyInputDevice() {
  KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
  dev->fd = -1;
  dev->evdev = NULL;

  return dev;
}

int InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  if(dev == NULL) return INPUT_DEV_INVALID_DEV;

  dev->fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (dev->fd < 0) {
    if (errno == EACCES)
      DEBUG_LOG("Fail to open file. You may need root permission.");
    return INPUT_DEV_INIT_ERROR;
  }

  int rc = libevdev_new_from_fd(dev->fd, &dev->evdev);
  if (rc < 0) return INPUT_DEV_INIT_ERROR;

  return INPUT_DEV_SUCCESS;
}

int SetKeyInputDetectCondition(KeyInputDevice dev, const struct input_event *ev) {
  if (dev == NULL) return INPUT_DEV_INVALID_DEV;
  // Should I validate ev, here?
  memcpy(&dev->target_event, ev, sizeof(struct input_event));
  return INPUT_DEV_SUCCESS;
}

static bool HasPendingEvent(struct libevdev *evdev, struct input_event *ev) {
  return libevdev_next_event(evdev, LIBEVDEV_READ_FLAG_NORMAL, ev)
          == LIBEVDEV_READ_STATUS_SUCCESS;
}

static bool IsTargetEvent(const struct input_event *target,
                          const struct input_event *ev) {
  return (target->type == ev->type
       && target->code == ev->code
       && target->value == ev->value);
}

int CheckKeyInput(KeyInputDevice dev) {
  if (dev == NULL || dev->evdev == NULL) return INPUT_DEV_INVALID_DEV;
  struct input_event ev = {};
  if (HasPendingEvent(dev->evdev, &ev) && IsTargetEvent(&dev->target_event, &ev)) {
    return INPUT_DEV_EVENT_DETECTED;
  }
  return INPUT_DEV_NO_EVENT;
}

int CleanupKeyInputDevice(KeyInputDevice dev) {
  if(dev == NULL) return INPUT_DEV_INVALID_DEV;

  libevdev_free(dev->evdev);
  dev->evdev = NULL;
  int rc = IO_CLOSE(dev->fd);
  if (rc < 0) return INPUT_DEV_CLEANUP_ERROR;

  return INPUT_DEV_SUCCESS;
}

void DestroyKeyInputDevice(KeyInputDevice dev) {
  if(dev == NULL) return;

  free(dev);
  dev = NULL;
}

led_driver.c


typedef struct LedDriverStruct {
  int fd;
  LedStatus status;
} LedDriverStruct;

LedDriver CreateLedDriver() {
  LedDriver led = calloc(1, sizeof(LedDriverStruct));
  led->fd = -1;
  led->status = LED_UNKNOWN;

  return led;
}

int InitLedDriver(LedDriver self, const char* device_file) {
  self->fd = IO_OPEN(device_file, O_WRONLY|O_NONBLOCK);
  if (self->fd < 0) {
    // TODO: Look into possible errors.
    return LED_DRIVER_INIT_ERROR;
  }

  return LED_DRIVER_SUCCESS;
}

void TurnOnLed(LedDriver self) {
  if (self == NULL) return;
  self->status = LED_TURN_ON;
  IO_WRITE(self->fd, "1\n", 2);
}

void TurnOffLed(LedDriver self) {
  if (self == NULL) return;
  self->status = LED_TURN_OFF;
  IO_WRITE(self->fd, "0\n", 2);
}

void ToggleLed(LedDriver self) {
  if (self == NULL || self->status == LED_UNKNOWN) return;

  if (self->status == LED_TURN_OFF) {
    TurnOnLed(self);
  } else {
    TurnOffLed(self);
  }
}

int CleanupLedDriver(LedDriver self) {
  if (self == NULL) return LED_DRIVER_CLEANUP_ERROR;

  int rc = IO_CLOSE(self->fd);
  if (rc < 0) {
    return LED_DRIVER_CLEANUP_ERROR;
  }
  return LED_DRIVER_SUCCESS;
}

void DestroyLedDriver(LedDriver self) {
  if (self == NULL) return;

  free(self);
  self = NULL;
}

main.c


#define KEYBOARD_DEVICE "/dev/input/event2"
#define LED_DEVICE      "/sys/class/leds/input2::capslock/brightness"

int main(void) {
  KeyInputDevice press_a = CreateKeyInputDevice();
  InitKeyInputDevice(press_a, KEYBOARD_DEVICE);
  struct timeval time = {};
  const struct input_event kPressA = {time, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
  SetKeyInputDetectCondition(press_a, &kPressA);

  LedDriver caps_led = CreateLedDriver();
  InitLedDriver(caps_led, LED_DEVICE);

  while(1) {
    if(CheckKeyInput(press_a) == INPUT_DEV_EVENT_DETECTED)
      ToggleLed(caps_led);
  }

  CleanupKeyInputDevice(press_a);
  DestroyKeyInputDevice(press_a);

  CleanupLedDriver(caps_led);
  DestroyLedDriver(caps_led);

  return 0;
}

Since it includes error handling, multi-instance support, etc., the functions are not equivalent, but the amount of code is more than tripled. In addition, the test code has more lines than the product code. This is because we are working on the problem in small units, so it is easy to see what happens in this case, and I think this is the result of implementing such a part (to some extent) seriously.

The total amount of code is large, but each function is short and cohesive, doing only one thing (single responsibility principle).

How can we guarantee that degreasing isn't happening when we add new features?

I have an existing test. If the existing tests pass, we are moving forward.

When you want to refactor, can you take the plunge?

Rely on tests for relentless refactoring. (I've refactored it a lot in the next minute, again thanks to the tests.)

What if your library is unstable?

The keystroke logic is closed in * key_input_event.c *. Also, main.c is ** unaware of the libevdev data structure **. Even if the usage of the library is changed slightly in key_input_event.c, the change is closed in the module and does not propagate to the upper module (closed in the principle of open / closed).

What if you connect another keyboard and now want to control the NumLock LED with the "B" key?

Initialization must be done individually. However, the detection part will be roughly as follows. (It's a little smarter if you bring a variable length array from the library)

  while(1) {
    for (int i = 0; i < 2; i ++) {
      if(CheckKeyInput(key_events[i]) == INPUT_DEV_EVENT_DETECTED)
        ToggleLed(led_devices[i]);
    }
  }

Looking at the API of key_input_event.c, if you have the following API, it seems that you can unify with the same interface.

I think that if the principle of single responsibility can be realized at the function level in this way, common parts can be easily found and abstraction can be promoted. Especially for immature things like me, it wasn't until I made one concrete thing that I thought, "Oh, if I abstract it like this, it's going to be well organized," or conversely, "I thought I could abstract it like this. It wasn't so good. " In Preparation, I wanted a callback mechanism, but I didn't need it at the moment (I'm worried about looping at 100% CPU). If so, it will be enough to sleep for about 10ms).

Source code

It is open to the public as Release1.0.

Future expansion

So far, we have achieved the following specifications.

Change this to the following specifications. You will need a timer as an element.

We will expand and develop this with TDD.

Recommended Posts

Let's develop something close to embedded with TDD ~ Intermediate review ~
Let's develop something close to embedded with TDD ~ Preparation ~
Let's develop something close to embedded with TDD ~ Problem raising ~
Let's develop something close to embedded with TDD ~ Design pattern ~
Let's develop something close to embedded with TDD ~ file open edition ~
Let's develop something close to embedded with TDD ~ Key input detection version ~
Let's develop something close to embedded with TDD ~ libevdev initialization / termination processing ~
[Introduction to WordCloud] Let's play with scraping ♬
[Introduction to Python] Let's use foreach with Python
Let's develop an investment algorithm with Python 1