Compare commits

...

7 Commits

Author SHA1 Message Date
9e00821802 Added image filename implementation 2026-06-25 17:44:58 +03:00
025cd56500 Some restructuring. Added structure for image finding 2026-06-25 17:01:30 +03:00
147fa762c8 Added API key generation 2026-06-25 16:06:13 +03:00
eca47d1596 Fixed memory problems 2026-06-25 16:04:56 +03:00
84bc7f5d40 Proper display endpoint 2026-06-23 16:14:24 +03:00
2e7899c701 Added basic diplay endpoint. Added images folder. 2026-06-23 15:31:35 +03:00
23e2fbd995 Moved TRMNL stuff to its own file 2026-06-22 18:11:31 +03:00
9 changed files with 631 additions and 162 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
server
*.json
images/*

View File

@@ -1,5 +1,6 @@
srcs += main.cpp
srcs += helpers.cpp
srcs += TRMNL.cpp
all:
g++ ${srcs} -o server

View File

@@ -20,5 +20,5 @@ Use file named `devices.json` in working directory or supply your own via config
* From: https://github.com/yhirose/cpp-httplib
* Version: 0.47.0
# Info on API
# Info on TRMNL API
* https://github.com/usetrmnl/trmnl-firmware

207
TRMNL.cpp Normal file
View File

@@ -0,0 +1,207 @@
#include "TRMNL.h"
#include "helpers.h"
using std::list;
using std::map;
using std::optional;
using std::string;
using nlohmann::json;
TRMNL::TRMNL(const string& id,
const string& api_key,
const string& friendly_id,
int refresh_rate)
: m_id(id),
m_api_key(api_key),
m_friendly_id(friendly_id),
m_refresh_rate(refresh_rate),
m_update_handler()
{}
TRMNL::TRMNL(const nlohmann::json& j)
: m_id(""),
m_api_key(""),
m_friendly_id(""),
m_refresh_rate(DEFAULT_REFRESH_RATE),
m_update_handler()
{
json_extract(j, "ID", m_id);
json_extract(j, "api_key", m_api_key);
json_extract(j, "friendly_id", m_friendly_id);
json_extract(j, "refresh_rate", m_refresh_rate);
}
const string& TRMNL::id() const
{
return m_id;
}
const string& TRMNL::api_key() const
{
return m_api_key;
}
const string& TRMNL::friendly_id() const
{
return m_friendly_id;
}
int TRMNL::refresh_rate() const
{
return m_refresh_rate;
}
void TRMNL::id(const std::string& id)
{
m_id = id;
if (m_update_handler)
{
m_update_handler(*this);
}
}
void TRMNL::api_key(const std::string& api_key)
{
m_api_key = api_key;
if (m_update_handler)
{
m_update_handler(*this);
}
}
void TRMNL::friendly_id(const std::string& friendly_id)
{
m_friendly_id = friendly_id;
if (m_update_handler)
{
m_update_handler(*this);
}
}
void TRMNL::refresh_rate(int refresh_rate)
{
m_refresh_rate = refresh_rate;
if (m_update_handler)
{
m_update_handler(*this);
}
}
void TRMNL::set_update_handler(std::function<void (const TRMNL& trmnl)> handler)
{
m_update_handler = handler;
}
string TRMNL::friendly_from_id(string id)
{
size_t pos = id.find(':');
while (pos != string::npos)
{
id.erase(pos, 1);
pos = id.find(':');
}
if (id.size() <= 6)
{
return id;
}
else
{
return id.substr(id.size() - 6);
}
}
void to_json(json& j, const TRMNL& trmnl)
{
j = json{
{"ID", trmnl.m_id},
{"api_key", trmnl.m_api_key},
{"friendly_id", trmnl.m_friendly_id},
{"refresh_rate", trmnl.m_refresh_rate},
};
}
void TRMNLContainer::TRMNL_update_handler(const TRMNL& trmnl)
{
auto it = m_by_id.find(trmnl.m_id);
if (it == m_by_id.end())
{
return;
}
// An update is needed only if friendly ID changes
if (0 == m_by_friendly.count(trmnl.m_friendly_id))
{
m_by_friendly[trmnl.m_friendly_id] = it->second;
}
}
TRMNLContainer& TRMNLContainer::operator=(const TRMNLContainer& other)
{
m_devices = other.m_devices;
m_by_id = other.m_by_id;
m_by_friendly = other.m_by_friendly;
for (TRMNL& device : m_devices)
{
device.set_update_handler([this](const TRMNL& trmnl){ TRMNL_update_handler(trmnl); });
}
return *this;
}
void TRMNLContainer::add_device(TRMNL trmnl)
{
if (trmnl.m_id.empty())
{
return;
}
auto it = m_devices.insert(m_devices.end(), trmnl);
m_by_id[trmnl.m_id] = it;
if (!trmnl.m_friendly_id.empty())
{
m_by_friendly[trmnl.m_friendly_id] = it;
}
it->set_update_handler([this](const TRMNL& trmnl){ TRMNL_update_handler(trmnl); });
}
TRMNL* TRMNLContainer::get_device_by_id(const string& id)
{
TRMNL* result = nullptr;
if (0 != m_by_id.count(id))
{
result = &(*m_by_id[id]);
}
return result;
}
TRMNL* TRMNLContainer::get_device_by_friendly(const string& friendly)
{
TRMNL* result = nullptr;
if (0 != m_by_friendly.count(friendly))
{
result = &(*m_by_friendly[friendly]);
}
return result;
}
void TRMNLContainer::clear()
{
m_by_friendly.clear();
m_by_id.clear();
m_devices.clear();
}
void to_json(json& j, const TRMNLContainer& cont)
{
j = cont.m_devices;
}

76
TRMNL.h Normal file
View File

@@ -0,0 +1,76 @@
#ifndef TRMNL_H_
#define TRMNL_H_
#include <functional>
#include <list>
#include <map>
#include <optional>
#include <string>
#include "json.hpp"
constexpr int DEFAULT_REFRESH_RATE = 300;
class TRMNL
{
private:
std::string m_id;
std::string m_api_key;
std::string m_friendly_id;
int m_refresh_rate;
std::function<void (const TRMNL& trmnl)> m_update_handler;
public:
// Constructors
TRMNL(const std::string& id = "",
const std::string& api_key = "",
const std::string& friendly_id = "",
int refresh_rate = DEFAULT_REFRESH_RATE);
TRMNL(const nlohmann::json& j);
// Getters
const std::string& id() const;
const std::string& api_key() const;
const std::string& friendly_id() const;
int refresh_rate() const;
// Setters
void id(const std::string& id);
void api_key(const std::string& api_key);
void friendly_id(const std::string& friendly_id);
void refresh_rate(int refresh_rate);
void set_update_handler(std::function<void (const TRMNL& trmnl)> handler);
static std::string friendly_from_id(std::string id);
friend void to_json(nlohmann::json& j, const TRMNL& trmnl);
friend class TRMNLContainer;
};
class TRMNLContainer
{
private:
std::list<TRMNL> m_devices;
std::map<std::string, std::list<TRMNL>::iterator> m_by_id;
std::map<std::string, std::list<TRMNL>::iterator> m_by_friendly;
void TRMNL_update_handler(const TRMNL& trmnl);
public:
TRMNLContainer() = default;
TRMNLContainer& operator=(const TRMNLContainer& other);
void add_device(TRMNL trmnl);
TRMNL* get_device_by_id(const std::string& id);
TRMNL* get_device_by_friendly(const std::string& friendly);
void clear();
friend void to_json(nlohmann::json& j, const TRMNLContainer& cont);
};
#endif // TRMNL_H_

View File

@@ -2,9 +2,11 @@
// Required
"host": "localhost",
"port": 0,
"base_url": "https://trmnl.com/",
// Optional
"devices_filename": "devices.json",
"folder_images": "images",
"cert_file": "path/to/file",
"key_file": "path/to/file"
}

View File

@@ -6,6 +6,7 @@ using nlohmann::json;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::istream;
using std::ostream;
using std::string;
@@ -18,7 +19,7 @@ bool read_file_json(json& j, const string& filename, ostream* log)
{
if (nullptr != log)
{
*log << "Could not open file" << endl;
*log << "Could not open file to read - " << filename << endl;
}
return false;
}
@@ -45,6 +46,24 @@ bool read_stream_json(json& j, istream& in, ostream* log)
return true;
}
bool write_file_json(const json& j, const string& filename, ostream* log)
{
ofstream file(filename);
if (!file.is_open())
{
if (nullptr != log)
{
*log << "Could not open file to write - " << filename << endl;
}
return false;
}
file << j.dump(4) << endl;
return true;
}
bool json_extract(const json& j, const string& key, string& out)
{
bool result = false;

View File

@@ -23,6 +23,13 @@ bool read_file_json(nlohmann::json& j, const std::string& filename, std::ostream
// Returns - true if read and parse is successful
bool read_stream_json(nlohmann::json& j, std::istream& in, std::ostream* log);
// Writes json to supplied filename
// j - input json
// filename - filepath to which to write json
// log - ostream to log human readable errors to - can be nullptr
// Returns - true if write is successful
bool write_file_json(const nlohmann::json& j, const std::string& filename, std::ostream* log);
// JSON Extraction Helpers
// out value is modified only if an extraction happened
// Returns - whether an extraction happened

448
main.cpp
View File

@@ -1,170 +1,201 @@
// Start using chrono when C++20 becomes available
// #include <chrono>
#include <filesystem>
#include <iostream>
#include <list>
#include <map>
#include <memory>
#include <random>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <stdio.h>
#include <stdint.h>
#include <time.h>
// Stop using these when C++20 becomes available
#include <fcntl.h>
#include <sys/stat.h>
#include "httplib.h"
#include "json.hpp"
#include "helpers.h"
#include "TRMNL.h"
using nlohmann::json;
using namespace std;
namespace fs = std::filesystem;
struct device
constexpr int API_KEY_LENGHT = 16;
constexpr char DEFAULT_IMAGE_URL[] = "https://trmnl.com/images/setup/setup-logo.bmp";
constexpr char DEFAULT_IMAGE_FNAME[] = "2024-09-20T00:00:00";
string generate_api_key(int len = API_KEY_LENGHT)
{
string id;
string api_key;
string friendly_id;
int refresh_rate;
string result = "";
std::random_device rand;
std::uniform_int_distribution distribution(0, 63);
int single;
char symbol;
// Default constructor
device()
: id(""),
api_key(""),
friendly_id(""),
refresh_rate(600)
{}
};
void to_json(json& j, const device& d)
if (len < 0)
{
j = json{
{"ID", d.id},
{"api_key", d.api_key},
{"friendly_id", d.friendly_id},
{"refresh_rate", d.refresh_rate}
};
len = API_KEY_LENGHT;
}
bool json_extract(const json& j, const string& key, device& out)
for (int i = 0; i < len; ++i)
{
bool result = false;
if (!key.empty())
single = distribution(rand);
if (single <= 9)
{
if (j.contains(key) && j[key].is_object())
{
result |= json_extract(j[key], "ID", out.id);
result |= json_extract(j[key], "api_key", out.api_key);
result |= json_extract(j[key], "friendly_id", out.friendly_id);
result |= json_extract(j[key], "refresh_rate", out.refresh_rate);
symbol = '0' + single;
}
else if ((single >= 10) && (single <= 35))
{
symbol = 'A' + single - 10;
}
else if ((single >= 36) && (single <= 61))
{
symbol = 'a' + single - 36;
}
else if (62 == single)
{
symbol = '+';
}
else if (63 == single)
{
symbol = '/';
}
// This should never happen
else
{
result |= json_extract(j, "ID", out.id);
result |= json_extract(j, "api_key", out.api_key);
result |= json_extract(j, "friendly_id", out.friendly_id);
result |= json_extract(j, "refresh_rate", out.refresh_rate);
symbol = '?';
}
result += symbol;
}
return result;
}
string device_id_to_friendly(string id)
bool reload_container(TRMNLContainer& container, const string& filename)
{
size_t pos = id.find(':');
while (pos != string::npos)
json j;
bool ok = read_file_json(j, filename, &cout);
if (!ok)
{
id.erase(pos, 1);
pos = id.find(':');
return false;
}
if (id.size() <= 6)
container.clear();
if (j.is_array())
{
return id;
}
else
for (int i = 0; i < j.size(); ++i)
{
return id.substr(id.size() - 6);
container.add_device(j[i]);
}
}
class DeviceContainer
{
private:
list<device> m_devices;
map<string, list<device>::iterator> m_by_id;
map<string, list<device>::iterator> m_by_friendly;
public:
DeviceContainer() = default;
void add_device(const device& d)
{
if (d.id.empty())
{
return;
return true;
}
auto it = m_devices.insert(m_devices.end(), d);
m_by_id.insert(make_pair(it->id, it));
if (!it->friendly_id.empty())
string get_timestamp_from_filename(const string& filepath)
{
m_by_friendly.insert(make_pair(it->friendly_id, it));
char result[128] = "";
struct stat status;
stat(filepath.c_str(), &status);
// Time of last modification
time_t file_time = status.st_mtim.tv_sec;
tm* file_tm;
file_tm = gmtime(&file_time);
snprintf(result, sizeof(result) - 1, "%04d-%02d-%02dT%02d:%02d:%02d",
file_tm->tm_year + 1900,
file_tm->tm_mon + 1,
file_tm->tm_mday,
file_tm->tm_hour,
file_tm->tm_min,
file_tm->tm_sec
);
return result;
}
// Use this when C++20 becomes available
#if 0
string get_timestamp_from_file_time(const fs::file_time_type& ftime)
{
char result[128] = "";
time_t file_time = std::chrono::system_clock::to_time_t(std::chrono::file_clock::to_sys(ftime));
tm* file_tm;
file_tm = gmtime(&file_time);
snprintf(result, sizeof(result) - 1, "%04d-%02d-%02dT%02d:%02d:%02d",
file_tm->tm_year + 1900,
file_tm->tm_mon + 1,
file_tm->tm_mday,
file_tm->tm_hour,
file_tm->tm_min,
file_tm->tm_sec
);
return result;
}
#endif
// First string is image filename for URL
// Second string is timestamp for TRMNL filename
pair<string, string> find_image_for_friendly(const string& folder_images, const string& friendly_id)
{
pair<string, string> result = {"", ""};
set<string> permitted_extensions = { ".bmp", ".png" };
if (!fs::is_directory(folder_images))
{
return result;
}
for (const fs::directory_entry& entry : fs::directory_iterator(folder_images))
{
if ((entry.path().stem() == friendly_id) &&
(0 != permitted_extensions.count(entry.path().extension())))
{
result.first = entry.path().filename();
result.second = get_timestamp_from_filename(entry.path());
// Change to this when C++20 becomes available
// result.second = get_timestamp_from_file_time(entry.last_write_time());
}
}
const device* get_device_by_id(const string& id)
{
if (0 == m_by_id.count(id))
{
return nullptr;
return result;
}
else
{
return &(*(*(m_by_id.find(id))).second);
}
}
const device* get_device_by_friendly(const string& friendly)
{
if (0 == m_by_friendly.count(friendly))
{
return nullptr;
}
else
{
return &(*(*(m_by_friendly.find(friendly))).second);
}
}
void set_device_friendly(const string& id, const string& friendly)
{
if (0 == m_by_id.count(id))
{
return;
}
auto it = m_by_id[id];
it->friendly_id = friendly;
m_by_friendly.insert(make_pair(it->friendly_id, it));
}
};
int main(int argc, char **argv)
{
string config_filename = "config.json";
string devices_filename = "devices.json";
string folder_images = "images";
string host = "";
uint16_t port = 0;
string base_url = "";
string cert_file = "";
string key_file = "";
uint16_t port = 0;
shared_ptr<httplib::Server> server = nullptr;
bool ok;
json cfg;
json devs;
vector<device> devices;
DeviceContainer container;
TRMNLContainer container;
if (argc > 2)
{
@@ -177,9 +208,14 @@ int main(int argc, char **argv)
return -1;
}
json_extract(cfg, "devices_filename", devices_filename);
json_extract(cfg, "host", host);
json_extract(cfg, "port", port);
json_extract(cfg, "base_url", base_url);
json_extract(cfg, "devices_filename", devices_filename);
json_extract(cfg, "folder_images", folder_images);
// TODO: Extract when SSL is ready
// json_extract(cfg, "cert_file", cert_file);
// json_extract(cfg, "key_file", cert_file);
@@ -195,6 +231,24 @@ int main(int argc, char **argv)
return -1;
}
if (base_url.empty())
{
cout << "base url not provided" << endl;
return -1;
}
if (folder_images.empty())
{
cout << "folder for images is empty" << endl;
return -1;
}
if (!fs::is_directory(folder_images))
{
cout << "filepath for images is not a folder" << endl;
return -1;
}
if (!cert_file.empty() && !key_file.empty())
{
// TODO: Implement SSL Server Properly
@@ -204,36 +258,39 @@ int main(int argc, char **argv)
server = make_shared<httplib::Server>();
}
ok = read_file_json(devs, devices_filename, &cout);
ok = reload_container(container, devices_filename);
if (!ok)
{
cout << "Could not read devices file" << endl;
return -1;
}
if (devs.is_array())
{
for (int i = 0; i < devs.size(); ++i)
{
device d;
json_extract(devs[i], "", d);
devices.push_back(d);
}
}
for (auto device : devices)
{
container.add_device(device);
}
auto setup_handler = [&container](const httplib::Request& req, httplib::Response& res)
{
if (req.has_header("ID"))
auto setup_handler = [&container, &devices_filename](const httplib::Request& req, httplib::Response& res)
{
json response;
if (!req.has_header("ID"))
{
// Bad Request - No ID header
res.status = 400;
response["status"] = 400;
response["error"] = "No ID header";
res.set_header("Content-Type", "application/json");
res.body = response.dump();
return;
}
// Refresh data from file
// Someone might have put a new device in
reload_container(container, devices_filename);
string id = req.get_header_value("ID");
const device* dev = container.get_device_by_id(id);
if (nullptr == dev)
TRMNL* trmnl = container.get_device_by_id(id);
if (nullptr == trmnl)
{
res.status = 404;
@@ -243,34 +300,126 @@ int main(int argc, char **argv)
response["image_url"] = nullptr;
response["filename"] = nullptr;
res.set_header("Content-Type", "application/json");
res.body = response.dump();
}
else
{
res.status = 200;
if (dev->friendly_id.empty())
{
container.set_device_friendly(id, device_id_to_friendly(id));
return;
}
device* dev_mut = const_cast<device*>(dev);
bool should_dump = false;
res.status = 200;
if (trmnl->friendly_id().empty())
{
should_dump = true;
trmnl->friendly_id(TRMNL::friendly_from_id(id));
}
if (trmnl->api_key().empty())
{
should_dump = true;
trmnl->api_key(generate_api_key());
}
response["status"] = 200;
response["api_key"] = "nullptr";
response["friendly_id"] = dev->friendly_id;
response["image_url"] = "https://trmnl.com/images/setup/setup-logo.bmp";
response["filename"] = "welcome";
response["api_key"] = trmnl->api_key();
response["friendly_id"] = trmnl->friendly_id();
response["image_url"] = DEFAULT_IMAGE_URL;
response["filename"] = DEFAULT_IMAGE_FNAME;
res.set_header("Content-Type", "application/json");
res.body = response.dump();
json test = *dev_mut;
cout << test.dump(4) << endl;
if (should_dump)
{
write_file_json(container, devices_filename, &cout);
}
};
auto display_handler = [&container, &devices_filename, &folder_images, &base_url]
(const httplib::Request& req, httplib::Response& res)
{
json response;
if (!req.has_header("ID") || !req.has_header("Access-Token"))
{
// Bad Request
res.status = 400;
response["status"] = 400;
response["error"] = "No ID and Access-Token headers set";
res.body = response.dump();
res.set_header("Content-Type", "application/json");
return;
}
// Refresh data from file
// Someone might have put a new device in
reload_container(container, devices_filename);
string id = req.get_header_value("ID");
string api_key = req.get_header_value("Access-Token");
TRMNL* trmnl = container.get_device_by_id(id);
if (nullptr == trmnl)
{
// Not Found
res.status = 404;
response["status"] = 404;
response["error"] = "Device not found";
res.set_header("Content-Type", "application/json");
res.body = response.dump();
return;
}
if (trmnl->api_key() != api_key)
{
// Forbidden
res.status = 403;
response["status"] = 403;
response["error"] = "Wrong credentials for this device";
res.set_header("Content-Type", "application/json");
res.body = response.dump();
return;
}
string image_fname;
string image_timestamp;
tie(image_fname, image_timestamp) = find_image_for_friendly(folder_images, trmnl->friendly_id());
// The ID and api_key match here
res.status = 200;
// From docs: will be 202 if no user_id is attached to device
response["status"] = 0;
if (image_fname.empty())
{
response["image_url"] = DEFAULT_IMAGE_URL;
response["filename"] = DEFAULT_IMAGE_FNAME;
}
else
{
res.status = 500;
string full_url = base_url;
full_url += "/images/";
full_url += image_fname;
response["image_url"] = full_url;
response["filename"] = image_timestamp;
}
response["refresh_rate"] = to_string(trmnl->refresh_rate());
// TODO: Handle firmware updating
response["update_firmware"] = false;
response["firmware_url"] = nullptr;
response["reset_firmware"] = false;
res.set_header("Content-Type", "application/json");
res.body = response.dump();
};
auto log_handler = [](const httplib::Request& req, httplib::Response& res)
@@ -287,12 +436,19 @@ int main(int argc, char **argv)
res.status = 200;
};
server->Get("/api/setup/", setup_handler);
server->Get("/api/setup", setup_handler);
server->Post("/api/log/", log_handler);
server->Post("/api/log", log_handler);
server->Get("/api/setup/*", setup_handler);
server->Get("/api/display/*", display_handler);
server->Post("/api/log/*", log_handler);
ok = server->set_mount_point("/images", folder_images);
if (!ok)
{
cout << "Could not mount images folder" << endl;
return -1;
}
cout << "Server listening on " << host << ":" << port << endl;
server->listen(host, port);
return 0;