aboutsummaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorElwin Hammer <elwinhammer@gmail.com>2024-05-29 21:41:24 +0200
committerGitHub <noreply@github.com>2024-05-29 21:41:24 +0200
commit1f78927e2e399a504368fb9b407de12d06dddcb5 (patch)
treef80ba30274ca75704075610a39fc28930f7ac4fa /client
parentd7616546dd5e8ba35c2b1b1ece736bca60e0b990 (diff)
parent8894d20ff0d1c1dde69879a21e756e01bcfa5262 (diff)
Merge pull request #11 from lonkaars/masterprot/software-puzzle
Bring software-puzzle up-to-date
Diffstat (limited to 'client')
-rw-r--r--client/CMakeLists.txt30
-rw-r--r--client/cmd.cpp94
-rw-r--r--client/cmd.h62
l---------client/compile_commands.json1
l---------client/lib1
-rw-r--r--client/main.cpp38
-rw-r--r--client/makefile2
-rw-r--r--client/parse.cpp176
-rw-r--r--client/parse.h42
l---------client/pbc1
-rw-r--r--client/readme.md37
-rw-r--r--client/rl.cpp86
-rw-r--r--client/rl.h10
-rw-r--r--client/sock.cpp125
-rw-r--r--client/sock.h34
-rw-r--r--client/xxd.c44
-rw-r--r--client/xxd.h17
17 files changed, 800 insertions, 0 deletions
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
new file mode 100644
index 0000000..57a2447
--- /dev/null
+++ b/client/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.29)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
+
+# enable debug features
+set(CMAKE_BUILD_TYPE Debug)
+add_compile_definitions(DEBUG)
+
+project(puzzlebox_client C CXX)
+
+include(../i2ctcp/include.cmake)
+
+add_executable(pbc
+ main.cpp
+ rl.cpp
+ sock.cpp
+ cmd.cpp
+ parse.cpp
+ xxd.c
+ )
+
+target_link_libraries(pbc
+ i2ctcp
+ mpack
+ readline # this is such a common library that I did not bother adding it as a submodule
+ )
+
+
diff --git a/client/cmd.cpp b/client/cmd.cpp
new file mode 100644
index 0000000..ab101e9
--- /dev/null
+++ b/client/cmd.cpp
@@ -0,0 +1,94 @@
+#include <cstdio>
+#include <cstdlib>
+#include <string.h>
+
+#include "cmd.h"
+#include "i2ctcpv1.h"
+#include "sock.h"
+#include "parse.h"
+
+#include "../shared/puzbus.h"
+
+char* consume_token(char* input, const char* ifs) {
+ strtok(input, ifs);
+ return strtok(NULL, "\0");
+}
+
+void cmd_exit(char*) {
+ exit(EXIT_SUCCESS);
+}
+
+void cmd_test(char*) {
+ const char* data = "Hello world!";
+ i2c_send(0x39, data, strlen(data));
+}
+
+void cmd_help(char*) {
+ printf("List of available commands:\n");
+ for (size_t i = 0; i < cmds_length; i++) {
+ struct cmd cmd = cmds[i];
+ printf(" %-*s", 10, cmd.name);
+ if (cmd.info != NULL)
+ printf(" %s", cmd.info);
+ printf("\n");
+ }
+
+ printf(
+ "\n"
+ "You can also use the TAB key to autocomplete commands\n"
+ );
+}
+
+void cmd_send(char* addr_str) {
+ char* data_str = consume_token(addr_str, IFS);
+
+ char* end;
+ uint16_t addr = strtol(addr_str, &end, 0);
+ if (addr_str + strlen(addr_str) != end) {
+ printf("address format error\n");
+ return;
+ }
+
+ char* data;
+ size_t data_size;
+ int err = strtodata(data_str, &data, &data_size);
+ if (err <= 0) {
+ printf("data format error at index %d:\n%s\n%*s^\n",
+ -err, data_str, -err, "");
+ return;
+ }
+
+ printf("sending char data[%lu = 0x%02lx] to 0x%02x\n", data_size, data_size, addr);
+ i2c_send(addr, data, data_size);
+
+ free(data);
+}
+
+void cmd_status(char*) {
+ const char msg[] = {
+ PB_CMD_READ,
+ 0x00, // addr 0 = global state
+ };
+ i2c_send(BUSADDR_MAIN, msg, sizeof(msg));
+ // NOTE: the reply handler will automatically print the state once it's
+ // received
+}
+
+void cmd_reset(char*) {
+ const char msg[] = {
+ PB_CMD_WRITE,
+ 0x00,
+ PB_GS_IDLE,
+ };
+ i2c_send(BUSADDR_MAIN, msg, sizeof(msg));
+}
+
+void cmd_ls(char*) {
+ return;
+ const char msg[] = {
+ PB_CMD_READ,
+ // TODO: which address is this?
+ };
+ i2c_send(BUSADDR_MAIN, msg, sizeof(msg));
+}
+
diff --git a/client/cmd.h b/client/cmd.h
new file mode 100644
index 0000000..932f3a2
--- /dev/null
+++ b/client/cmd.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <stddef.h>
+
+typedef void cmd_fn_t(char *);
+
+struct cmd {
+ void (* handle)(char *);
+ const char* name;
+ const char* info;
+ // TODO: tab completion function?
+};
+
+cmd_fn_t cmd_exit;
+cmd_fn_t cmd_test;
+cmd_fn_t cmd_help;
+cmd_fn_t cmd_status;
+cmd_fn_t cmd_reset;
+cmd_fn_t cmd_ls;
+cmd_fn_t cmd_send;
+
+static const struct cmd cmds[] = {
+ {
+ .handle = cmd_exit,
+ .name = "exit",
+ .info = "exit pbc",
+ },
+ {
+ .handle = cmd_test,
+ .name = "test",
+ .info = "send a test puzbus message",
+ },
+ {
+ .handle = cmd_help,
+ .name = "help",
+ .info = "show this help",
+ },
+ {
+ .handle = cmd_status,
+ .name = "status",
+ .info = "show global puzzle box state (main controller state)",
+ },
+ {
+ .handle = cmd_reset,
+ .name = "reset",
+ .info = "reset entire game state",
+ },
+ {
+ .handle = cmd_ls,
+ .name = "ls",
+ .info = "list connected puzzle modules",
+ },
+#ifdef DEBUG
+ {
+ .handle = cmd_send,
+ .name = "send",
+ .info = "[debug] send raw message",
+ },
+#endif
+};
+static const size_t cmds_length = sizeof(cmds) / sizeof(cmds[0]);
+
diff --git a/client/compile_commands.json b/client/compile_commands.json
new file mode 120000
index 0000000..25eb4b2
--- /dev/null
+++ b/client/compile_commands.json
@@ -0,0 +1 @@
+build/compile_commands.json \ No newline at end of file
diff --git a/client/lib b/client/lib
new file mode 120000
index 0000000..dc598c5
--- /dev/null
+++ b/client/lib
@@ -0,0 +1 @@
+../lib \ No newline at end of file
diff --git a/client/main.cpp b/client/main.cpp
new file mode 100644
index 0000000..5c26107
--- /dev/null
+++ b/client/main.cpp
@@ -0,0 +1,38 @@
+#include <cstdio>
+#include <cstdlib>
+#include <cstdint>
+#include <exception>
+
+#include "rl.h"
+#include "sock.h"
+
+PBSocket* sock;
+
+int main(int argc, char** argv) {
+ if (argc < 2) {
+ printf("usage: %s addr [port]\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ // parse arguments
+ char* addr = argv[1];
+ uint16_t port = 9191;
+ if (argc >= 3) port = atoi(argv[2]);
+
+ sock = new PBSocket(addr, port);
+ try {
+ // connect to TCP socket (automatically spawns thread)
+ sock->sock_connect();
+ } catch (const std::exception& e) {
+ printf("error: %s\n", e.what());
+ return EXIT_FAILURE;
+ }
+
+ // enter main CLI (using GNU readline for comfyness)
+ int ret = cli_main();
+
+ delete sock;
+
+ return ret;
+}
+
diff --git a/client/makefile b/client/makefile
new file mode 100644
index 0000000..8352615
--- /dev/null
+++ b/client/makefile
@@ -0,0 +1,2 @@
+include ../lazy.mk
+
diff --git a/client/parse.cpp b/client/parse.cpp
new file mode 100644
index 0000000..f31e802
--- /dev/null
+++ b/client/parse.cpp
@@ -0,0 +1,176 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <netinet/in.h>
+
+#include "parse.h"
+
+static int parse_string(const char * str, char * data, size_t * offset) {
+ char closing = str[0];
+ char escape = false;
+ int i = 0;
+ size_t len = strlen(str);
+
+ switch (str[i]) {
+ case '\'':
+ escape = false;
+ break;
+ case '\"':
+ escape = true;
+ break;
+ default:
+ return -i;
+ }
+
+ for (i = 1; i < len && str[i] != '\0'; i++, *offset += 1) {
+ char c = str[i];
+
+ // TODO: handle escaped characters
+
+ if (c == closing)
+ return i + 1; // +1 for closing quote
+
+ if (data != NULL)
+ data[*offset] = c;
+ }
+
+ return -i;
+}
+
+static int parse_hexstr(const char * str, char * data, size_t * offset) {
+ const char* ifs = IFS;
+ size_t len = strcspn(str, ifs);
+ int i = 0;
+
+ // check if token contains at least one colon
+ const char* colon = strchr(str, ':');
+ if (colon == NULL) return -i;
+ if (colon >= str + len) return -i;
+
+ // check if token only contains allowed characters [0-9a-fA-F:]
+ size_t len_ok = strspn(str + i, SET_HEX_STR) + i;
+ if (len != len_ok) return -len_ok;
+
+ size_t c = 0;
+ while (c < len) { // count bytes in bytestring
+ if (strspn(str + c, SET_HEX) != 2)
+ return -i -c;
+
+ if (data != NULL)
+ data[*offset] = strtol(str + c, NULL, 16) & 0xff;
+
+ c += 2;
+ *offset += 1;
+
+ if (str[c] == ':') {
+ c += 1;
+ continue;
+ }
+ break;
+ }
+
+ i += len;
+ return i;
+}
+
+static int parse_number(const char * str, char * data, size_t * offset) {
+ const char* ifs = IFS;
+ size_t len = strcspn(str, ifs);
+ int i = 0;
+ int base = 10;
+ bool bytestring = false;
+
+ if (len > 2 && strncmp(str, "0x", 2) == 0) { // hexadecimal prefix
+ base = 16;
+ i += 2;
+ }/* else if (len > 1 && strncmp(str, "0", 1) == 0) { // octal prefix
+ base = 8;
+ i += 1;
+ }*/
+
+ const char* set;
+ // if (base == 8) set = SET_OCT;
+ if (base == 10) set = SET_DEC;
+ if (base == 16) set = SET_HEX;
+
+ size_t len_ok = strspn(str + i, set) + i;
+ if (len != len_ok) return -len_ok;
+
+ size_t size = 1; // default integer size in bytes
+ if (base == 16) {
+ size_t prefixless = len - i;
+ switch (prefixless) {
+ case 2: // 8-bit (2 hex characters)
+ case 4: // 16-bit
+ case 8: // 32-bit
+ case 16: // 64-bit
+ break;
+ default:
+ return -i;
+ }
+ size = prefixless / 2;
+ }
+
+ if (data != NULL) {
+ unsigned long number = strtol(str + i, NULL, base);
+ long long mask = (1 << 8 * size) - 1;
+ number &= mask;
+ // NOTE: the hton? functions are used to convert host endianness to network
+ // endianness (big), and are required
+ switch (size) {
+ case 1:
+ data[*offset] = number & 0xff;
+ break;
+ case 2:
+ number = htons(number);
+ // TODO: check if the endianness is OK, or reverse these *offset indices*
+ data[*offset + 1] = (number) & 0xff;
+ data[*offset + 0] = (number >>= 8) & 0xff;
+ break;
+ case 4:
+ number = htonl(number);
+ data[*offset + 3] = (number) & 0xff;
+ data[*offset + 2] = (number >>= 8) & 0xff;
+ data[*offset + 1] = (number >>= 8) & 0xff;
+ data[*offset + 0] = (number >>= 8) & 0xff;
+ break;
+ }
+ }
+
+ *offset += size;
+ return len;
+}
+
+static int _strtodata_main(const char * str, char* data, size_t * offset) {
+ const char* ifs = IFS;
+ size_t len = strlen(str);
+
+ int i, run;
+ for (i = 0; i < len; i += run) {
+ i += strspn(&str[i], ifs); // skip whitespace
+ if (str[i] == '\0') break; // end of string
+
+ if ((run = parse_string(str + i, data, offset)) > 0) continue;
+ if ((run = parse_hexstr(str + i, data, offset)) > 0) continue;
+ if ((run = parse_number(str + i, data, offset)) > 0) continue;
+
+ // no format detected
+ return -i + run;
+ }
+
+ return i;
+}
+
+int strtodata(const char * str, char ** data, size_t * size) {
+ *size = 0;
+
+ // 1st pass: check data format
+ int ret = _strtodata_main(str, NULL, size);
+ if (ret <= 0) return ret; // on error
+
+ // 2nd pass: convert string literals into binary data
+ *data = (char*) malloc(*size);
+ size_t written = 0;
+ return _strtodata_main(str, *data, &written);
+}
+
diff --git a/client/parse.h b/client/parse.h
new file mode 100644
index 0000000..94afe70
--- /dev/null
+++ b/client/parse.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stddef.h>
+
+#define IFS " \t\n"
+
+#define SET_OCT "01234567"
+#define SET_DEC "0123456789"
+#define SET_HEX SET_DEC"abcdefABCDEF"
+#define SET_HEX_STR SET_HEX":"
+
+/**
+ * \brief modify \p token to point to the first token when broken up on \p ifs
+ * and return the remaining data
+ *
+ * \p token will be null-terminated to indicate the end of the first token. A
+ * pointer to the remaining line after the NULL byte is returned, or NULL when
+ * the end of the string has been reached.
+ *
+ * \param token input string
+ * \param ifs string containing field separators
+ *
+ * \return the remaining data after \p token and the first \p ifs
+ */
+char* consume_token(char * token, const char * ifs);
+
+/**
+ * \brief convert string with literals into raw data
+ *
+ * \param str input string containing literals
+ * \param data pointer to \c char* that will store the resulting data
+ * \param size size of \p data
+ *
+ * \return 0 or a negative integer representing the index where there is a
+ * syntax error if there was an error, or a positive integer representing the
+ * amount of bytes parsed from \p str
+ *
+ * \note The pointer that \p data refers to will not be initialized by this
+ * function if parsing fails
+ */
+int strtodata(const char * str, char ** data, size_t * size);
+
diff --git a/client/pbc b/client/pbc
new file mode 120000
index 0000000..51eda50
--- /dev/null
+++ b/client/pbc
@@ -0,0 +1 @@
+build/pbc \ No newline at end of file
diff --git a/client/readme.md b/client/readme.md
new file mode 100644
index 0000000..ea3e034
--- /dev/null
+++ b/client/readme.md
@@ -0,0 +1,37 @@
+# puzzle box client
+
+This folder contains the source code for the puzzle box client (pbc). This is a
+desktop application that communicates with the main controller over TCP to
+send/receive I<sup>2</sup>C messages. This application is not only used by a
+game operator to control and monitor the state of a puzzle box, but is also a
+useful debugging tool when developing puzzle modules, as it allows you to send
+arbitrary data over the puzzle bus.
+
+## Features
+
+- List detected puzzle modules
+- Reset puzzle modules (individually or all to reset the box)
+- Skip puzzle modules (individually or all)
+- Request puzzle box state
+
+Debug only:
+- Send arbitrary messages
+
+## Building
+
+PBC is a standard CMake project, but a [makefile](./makefile) is provided for
+convenience (still requires CMake and Ninja are installed).
+
+## Send data
+
+```
+ ADDRESS DATA
+ v~~~ v~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+send 0x39 68:65:6c:6c:6f 44 0x20 'world' 33
+ ^~~~~~~~~~~~~~ ^~ ^~~~ ^~~~~~~ ^~
+ HEXSTR NUMBER NUMBER STRING NUMBER
+ (binary) (dec) (hex) (literal) (dec)
+```
+
+The data is concatenated, and may contain mixed types of literals
+
diff --git a/client/rl.cpp b/client/rl.cpp
new file mode 100644
index 0000000..3f93e99
--- /dev/null
+++ b/client/rl.cpp
@@ -0,0 +1,86 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "rl.h"
+#include "cmd.h"
+#include "parse.h"
+
+void rl_printf(const char *fmt, ...) {
+ // save line
+ char* saved_line = rl_copy_text(0, rl_end);
+ int saved_point = rl_point;
+ int saved_end = rl_end;
+
+ // clear line
+ rl_save_prompt();
+ rl_replace_line("", 0);
+ rl_redisplay();
+
+ // printf
+ va_list args;
+ va_start(args, fmt);
+ vprintf(fmt, args);
+ va_end(args);
+
+ // restore line
+ rl_restore_prompt();
+ rl_replace_line(saved_line, 0);
+ rl_point = saved_point;
+ rl_end = saved_end;
+ rl_redisplay();
+
+ free(saved_line);
+}
+
+static void cli_cmd(char* cmd) {
+ char* line = consume_token(cmd, IFS);
+
+ for (size_t i = 0; i < cmds_length; i++) {
+ if (strncmp(cmds[i].name, cmd, strlen(cmd)) != 0)
+ continue;
+
+ cmds[i].handle(line);
+ return;
+ }
+
+ printf("unknown command!\n");
+}
+
+static char* rl_completion_entries(const char *text, int state) {
+ static size_t i = 0;
+ if (state == 0) i = 0;
+
+ while (i < cmds_length) {
+ struct cmd cmd = cmds[i];
+ i++;
+ if (strncmp(text, cmd.name, strlen(text)) == 0) {
+ return strdup(cmd.name);
+ }
+ }
+
+ return NULL;
+}
+
+int cli_main() {
+ char* input = NULL;
+ rl_completion_entry_function = rl_completion_entries;
+
+ while (1) {
+ if (input != NULL) free(input);
+ input = readline(CLI_PROMPT);
+
+ if (input == NULL) return EXIT_SUCCESS; // exit on ^D (EOF)
+ if (*input == '\0') continue; // ignore empty lines
+ add_history(input);
+
+ cli_cmd(input);
+ }
+
+ return EXIT_SUCCESS;
+}
+
diff --git a/client/rl.h b/client/rl.h
new file mode 100644
index 0000000..503225f
--- /dev/null
+++ b/client/rl.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#define COLOR_OFF "\x1b[0m"
+#define COLOR_BOLD "\x1b[1m"
+
+#define CLI_PROMPT "(" COLOR_BOLD "pbc" COLOR_OFF ") "
+
+int cli_main();
+void rl_printf(const char *fmt, ...);
+
diff --git a/client/sock.cpp b/client/sock.cpp
new file mode 100644
index 0000000..2d5787d
--- /dev/null
+++ b/client/sock.cpp
@@ -0,0 +1,125 @@
+#include <arpa/inet.h>
+#include <cstring>
+#include <stdexcept>
+#include <unistd.h>
+#include <cstdio>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <errno.h>
+
+#include <thread>
+
+#include "i2ctcpv1.h"
+#include "sock.h"
+#include "rl.h"
+
+using std::logic_error;
+using std::thread;
+
+PBSocket::PBSocket() { }
+PBSocket::PBSocket(const char * addr, uint16_t port) : PBSocket() {
+ set_server(addr, port);
+}
+
+PBSocket::~PBSocket() {
+ // stop TCP listen thread
+ if (_thread != nullptr) {
+ _thread->detach();
+ delete _thread;
+ }
+
+ sock_close();
+}
+
+void PBSocket::set_server(const char * addr, uint16_t port) {
+ _addr = addr;
+ _port = port;
+}
+
+void PBSocket::sock_connect() {
+ if (_addr == NULL) throw logic_error("no server address defined");
+ if (_port == 0) throw logic_error("no server port defined");
+
+ if (_thread != nullptr) throw logic_error("already connected");
+
+ rl_printf("connecting to %s on port %d...\n", _addr, _port);
+
+ _fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (_fd < 0) throw logic_error("socket create failed");
+
+ struct sockaddr_in server = {
+ .sin_family = AF_INET,
+ .sin_port = htons(_port),
+ .sin_addr = {
+ .s_addr = inet_addr(_addr),
+ },
+ };
+ int ret = connect(_fd, (struct sockaddr*) &server, sizeof(server));
+ if (ret != 0) throw logic_error(strerror(errno));
+
+ this->_thread = new thread(&PBSocket::sock_task, this);
+}
+
+void PBSocket::sock_close() {
+ if (_fd < 0) return; // already closed
+ close(_fd);
+ _fd = -1;
+}
+
+void PBSocket::send(const char * buf, size_t buf_sz) {
+ write(_fd, buf, buf_sz);
+}
+
+void PBSocket::sock_task() {
+ i2ctcp_msg_t input;
+
+ while(1) {
+ char buf[80];
+ ssize_t bytes = read(_fd, buf, sizeof(buf));
+
+ if (bytes == -1) {
+ rl_printf("error: %s (%d)\n", strerror(errno), errno);
+ break;
+ }
+
+ // skip empty frames
+ if (bytes == 0) continue;
+
+ int ret = i2ctcp_read(&input, buf, bytes);
+
+ // header read error
+ if (ret < 0) {
+ rl_printf("i2ctcp_read error!\n");
+ break;
+ }
+
+ // continue reading if more bytes needed...
+ if (ret > 0) continue;
+
+ // message read completely!
+ i2c_recv(input.addr, input.data, input.length);
+ free(input.data);
+ }
+
+ sock_close();
+}
+
+void i2c_send(uint16_t addr, const char * data, size_t data_size) {
+ i2ctcp_msg_t msg = {
+ .addr = addr,
+ .data = (char *) data,
+ .length = data_size,
+ };
+
+ char* packed;
+ size_t size;
+ if (!i2ctcp_write(&msg, &packed, &size)) return;
+
+ sock->send(packed, size);
+}
+
+void i2c_recv(uint16_t addr, const char * data, size_t data_size) {
+ rl_printf("[0x%02x]: %.*s\n", addr, data_size, data);
+}
+
diff --git a/client/sock.h b/client/sock.h
new file mode 100644
index 0000000..42eba3b
--- /dev/null
+++ b/client/sock.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <cstdint>
+#include <thread>
+
+class PBSocket {
+public:
+ PBSocket();
+ PBSocket(const char * addr, uint16_t port);
+ virtual ~PBSocket();
+
+ void set_server(const char * addr, uint16_t port);
+
+ void sock_connect();
+
+ void send(const char * buf, size_t buf_sz);
+
+private:
+ void sock_task();
+ void sock_close();
+
+ std::thread* _thread = nullptr;
+
+ const char * _addr = NULL;
+ uint16_t _port = 0;
+
+ int _fd = -1;
+};
+
+extern PBSocket* sock;
+
+void i2c_send(uint16_t addr, const char * data, size_t data_size);
+void i2c_recv(uint16_t addr, const char * data, size_t data_size);
+
diff --git a/client/xxd.c b/client/xxd.c
new file mode 100644
index 0000000..5d83635
--- /dev/null
+++ b/client/xxd.c
@@ -0,0 +1,44 @@
+#include <stdio.h>
+#include <ctype.h>
+
+#include "xxd.h"
+
+void xxd(const char * data, size_t size) {
+ size_t fake_size = size + (16 - size % 16) % 16;
+
+ for (size_t base = 0; base < fake_size; base += 16) {
+ printf("%08lx: ", base);
+
+ // print bytes
+ for (size_t offset = 0; offset < 16; offset++) {
+ size_t i = base + offset;
+
+ if (offset == 8) printf(" ");
+
+ if (i >= size) {
+ printf(" ");
+ continue;
+ }
+
+ printf("%02x ", data[i] & 0xff);
+ }
+
+ // print ascii representation
+ printf(" |");
+ for (size_t offset = 0; offset < 16; offset++) {
+ size_t i = base + offset;
+
+ if (i >= size) {
+ printf(" ");
+ continue;
+ }
+
+ if (isprint(data[i]))
+ printf("%c", data[i]);
+ else
+ printf(".");
+ }
+ printf("|\n");
+ }
+}
+
diff --git a/client/xxd.h b/client/xxd.h
new file mode 100644
index 0000000..fb28bb1
--- /dev/null
+++ b/client/xxd.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \brief utility function that prints hexdump of data
+ */
+void xxd(const char * data, size_t size);
+
+#ifdef __cplusplus
+}
+#endif
+