Skip to content

Commit

Permalink
Merge c295864 into ca5ab8a
Browse files Browse the repository at this point in the history
  • Loading branch information
ubgk committed Aug 13, 2023
2 parents ca5ab8a + c295864 commit 941b246
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 3 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ All notable changes to this project will be documented in this file.
## [1.3.0] - 2023/07/24

### Added

- Portable and SSH-compatible Keyboard source
- Support macOS operating systems (with help from @boragokbakan)
- Virtual destructor to the main ``Interface`` class

Expand Down
14 changes: 12 additions & 2 deletions observation/sources/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,21 @@ cc_library(
include_prefix = "vulp/observation/sources",
)

cc_library(
name = "keyboard",
hdrs = ["Keyboard.h"],
srcs = ["Keyboard.cpp"],
deps = [
"//observation:source",
],
include_prefix = "vulp/observation/sources",
)

cc_library(
name = "sources",
deps = select({
"@//:linux": [":joystick", ":cpu_temperature"],
"@//conditions:default": [":cpu_temperature"],
"@//:linux": [":joystick", ":cpu_temperature", ":keyboard"],
"@//conditions:default": [":cpu_temperature", ":keyboard"],
}),
)

Expand Down
129 changes: 129 additions & 0 deletions observation/sources/Keyboard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2023 Inria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "vulp/observation/sources/Keyboard.h"

namespace vulp::observation::sources {
Keyboard::Keyboard() {
termios term;
tcgetattr(STDIN_FILENO, &term);
term.c_lflag &= ~ICANON;
tcsetattr(STDIN_FILENO, TCSANOW, &term);
setbuf(stdin, NULL);

key_pressed_ = false;
key_code_ = Key::UNKNOWN;

last_key_poll_time_ = system_clock::now() - milliseconds(kPollingIntervalMS);
}

Keyboard::~Keyboard() {}

bool Keyboard::read_event() {
ssize_t bytes_available = 0;
ioctl(STDIN_FILENO, FIONREAD, &bytes_available);

if (bytes_available) {
int bytes_read = ::read(STDIN_FILENO, &buf_, (ssize_t)bytes_available);

// DEBUG
printf("Read %d/%d bytes from stdin: ", bytes_read, bytes_available);
for (int i = 0; i < bytes_read; i++) {
printf("%d [%02x]", buf_[i], buf_[i]);
}
printf("\n");

if (bytes_read != bytes_available) {
spdlog::warn("All bytes could not be read from the standard input!");
::fflush(stdin);
}

return 1;
}

return 0;
}

Key Keyboard::map_char_to_key(unsigned char* buf) {
// Check for 3-byte characters first (i.e. arrows)
if (!memcmp(buf_, DOWN_BYTES, kMaxKeyBytes)) {
return Key::DOWN;
}
if (!memcmp(buf_, UP_BYTES, kMaxKeyBytes)) {
return Key::UP;
}
if (!memcmp(buf_, LEFT_BYTES, kMaxKeyBytes)) {
return Key::LEFT;
}
if (!memcmp(buf_, RIGHT_BYTES, kMaxKeyBytes)) {
return Key::RIGHT;
}

// If the first byte corresponds to a lowercase ASCII alphabetic
if (is_lowercase_alpha(buf[0])) {
buf[0] -= 32; // Map to uppercase equivalent
}

// We treat any printable ASCII as a single key code
if (is_printable_ascii(buf[0])) {
switch (buf[0]) {
case 87: // 0x57
return Key::W;
case 65: // 0x41
return Key::A;
case 83: // 0x53
return Key::S;
case 68: // 0x44
return Key::D;
case 88: // 0x58
return Key::X;
}
}
return Key::UNKNOWN;
}

void Keyboard::write(Dictionary& observation) {
// Check elapsed time since last key polling
auto elapsed = system_clock::now() - last_key_poll_time_;
auto elapsed_ms = duration_cast<milliseconds>(elapsed).count();

// Poll for key press if enough time has elapsed or if no key is pressed
if (elapsed_ms >= kPollingIntervalMS || !key_pressed_) {
key_pressed_ = read_event();

if (key_pressed_) {
key_code_ = map_char_to_key(buf_);
} else {
key_code_ = Key::UNKNOWN;
}

last_key_poll_time_ = system_clock::now();
}

auto& output = observation(prefix());
output("key_pressed") = key_pressed_;
output("up") = key_code_ == Key::UP;
output("down") = key_code_ == Key::DOWN;
output("left") = key_code_ == Key::LEFT;
output("right") = key_code_ == Key::RIGHT;
output("w") = key_code_ == Key::W;
output("a") = key_code_ == Key::A;
output("s") = key_code_ == Key::S;
output("d") = key_code_ == Key::D;
output("x") = key_code_ == Key::X;
}

} // namespace vulp::observation::sources
128 changes: 128 additions & 0 deletions observation/sources/Keyboard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2023 Inria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <termios.h>
#include <unistd.h>

#include <chrono>
#include <iostream>
#include <string>

#include "vulp/observation/Source.h"

using std::chrono::duration_cast;
using std::chrono::milliseconds;
using std::chrono::system_clock;

//! Maximum number of bytes to encode a key
constexpr size_t kMaxKeyBytes = 3;

//! Polling interval in milliseconds
constexpr int64_t kPollingIntervalMS = 100;

// Byte sequences that encode arrow keys are platform specific
#ifndef __APPLE__
constexpr unsigned char UP_BYTES[] = {0xEF, 0x9C, 0x80};
constexpr unsigned char DOWN_BYTES[] = {0xEF, 0x9C, 0x81};
constexpr unsigned char RIGHT_BYTES[] = {0xEF, 0x9C, 0x83};
constexpr unsigned char LEFT_BYTES[] = {0xEF, 0x9C, 0x82};
#else
constexpr unsigned char UP_BYTES[] = {0x1B, 0x5B, 0x41};
constexpr unsigned char DOWN_BYTES[] = {0x1B, 0x5B, 0x42};
constexpr unsigned char RIGHT_BYTES[] = {0x1B, 0x5B, 0x43};
constexpr unsigned char LEFT_BYTES[] = {0x1B, 0x5B, 0x44};
#endif

inline bool is_lowercase_alpha(unsigned char c) {
return 0x61 <= c && c <= 0x7A;
}
inline bool is_uppercase_alpha(unsigned char c) {
return 0x41 <= c && c <= 0x5A;
}
inline bool is_printable_ascii(unsigned char c) {
return 0x20 <= c && c <= 0x7F;
}

namespace vulp::observation::sources {
enum class Key {
UP,
DOWN,
LEFT,
RIGHT,
W,
A,
S,
D,
X,
UNKNOWN // Map everything else to this key
};

/*! Source for reading Keyboard inputs.
*
* \note This source reads from the standard input, and does
* not listen to Keyboard events. It can only read one key at a time.
*/
class Keyboard : public Source {
public:
/*! Constructor sets up the terminal in non-canonical mode where
* input is available immediately without waiting for a newline.
*/
Keyboard();

//! Destructor
~Keyboard() override;

//! Prefix of output in the observation dictionary.
inline std::string prefix() const noexcept final { return "keyboard"; }

/*! Write output to a dictionary.
*
* \param[out] output Dictionary to write observations to.
*/
void write(Dictionary& output) final;

private:
//! Read the next key event from STDIN.
bool read_event();

/*! Map a character to a key code.
*
* \param[in] buf Buffer containing the character.
* \return Key value
*/
Key map_char_to_key(unsigned char* buf);

//! Buffer to store incoming bytes from STDIN
unsigned char buf_[kMaxKeyBytes];

//! Key code of the last key pressed
Key key_code_;

//! Whether the last key pressed is still pressed
bool key_pressed_;

//! Last time a key was pressed in milliseconds
system_clock::time_point last_key_poll_time_;
};

} // namespace vulp::observation::sources
104 changes: 104 additions & 0 deletions observation/sources/tests/KeyboardTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2023 Inria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <fstream>

#include "gtest/gtest.h"
#include "vulp/observation/sources/Keyboard.h"

namespace vulp::observation::sources {

TEST(Keyboard, WriteOnce) {
Keyboard keyboard;
Dictionary observation;
ASSERT_NO_THROW(keyboard.write(observation));
}

TEST(Keyboard, ReadAlphabetical) {
// We cannot write directly to STDIN, so we'll redirect a file to it
char* tmpfn = tmpnam(nullptr);
std::ofstream tmpf(tmpfn);
tmpf << "A"; //(unsigned char[]){'A'};
tmpf.close();

freopen(tmpfn, "r", stdin);

Keyboard keyboard = Keyboard();

Dictionary observation;
keyboard.write(observation);

const auto& output = observation(keyboard.prefix());
ASSERT_TRUE(output.get<bool>("key_pressed"));
ASSERT_FALSE(output.get<bool>("left"));
ASSERT_FALSE(output.get<bool>("up"));
ASSERT_FALSE(output.get<bool>("down"));
ASSERT_FALSE(output.get<bool>("right"));
ASSERT_FALSE(output.get<bool>("w"));
ASSERT_TRUE(output.get<bool>("a"));
ASSERT_FALSE(output.get<bool>("s"));
ASSERT_FALSE(output.get<bool>("d"));
ASSERT_FALSE(output.get<bool>("x"));
}

TEST(Keyboard, ReadArrows) {
// We cannot write directly to STDIN, so we'll redirect a file to it
char* tmpfn = tmpnam(nullptr);
std::ofstream tmpf(tmpfn);
tmpf << LEFT_BYTES;
tmpf.close();

freopen(tmpfn, "r", stdin);

Keyboard keyboard = Keyboard();

Dictionary observation;
keyboard.write(observation);

const auto& output = observation(keyboard.prefix());
ASSERT_TRUE(output.get<bool>("key_pressed"));
ASSERT_TRUE(output.get<bool>("left"));
ASSERT_FALSE(output.get<bool>("up"));
ASSERT_FALSE(output.get<bool>("down"));
ASSERT_FALSE(output.get<bool>("right"));
ASSERT_FALSE(output.get<bool>("w"));
ASSERT_FALSE(output.get<bool>("a"));
ASSERT_FALSE(output.get<bool>("s"));
ASSERT_FALSE(output.get<bool>("d"));
ASSERT_FALSE(output.get<bool>("x"));
}

TEST(Keyboard, ReadEmptySTDIN) {
fflush(stdin); // Clear STDIN

Keyboard keyboard = Keyboard();

Dictionary observation;
keyboard.write(observation);

const auto& output = observation(keyboard.prefix());
ASSERT_FALSE(output.get<bool>("key_pressed"));
ASSERT_FALSE(output.get<bool>("left"));
ASSERT_FALSE(output.get<bool>("up"));
ASSERT_FALSE(output.get<bool>("down"));
ASSERT_FALSE(output.get<bool>("right"));
ASSERT_FALSE(output.get<bool>("w"));
ASSERT_FALSE(output.get<bool>("a"));
ASSERT_FALSE(output.get<bool>("s"));
ASSERT_FALSE(output.get<bool>("d"));
ASSERT_FALSE(output.get<bool>("x"));
}

} // namespace vulp::observation::sources

0 comments on commit 941b246

Please sign in to comment.