we're getttng C to work

C deez nuts
This commit is contained in:
2026-05-02 19:22:29 +02:00
parent 5f7d4136cd
commit 96e14ed6ea
27 changed files with 1287 additions and 2187 deletions

View File

@@ -1,154 +0,0 @@
#include "LCDDisplay.hpp"
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <log.hpp>
#include <string>
/* lcd size hardcoded to 4x40 idgaf */
LCDDisplay::LCDDisplay(const std::string& dev, const uint32_t baud) {
this->dev = dev;
this->baud = baud;
}
LCDDisplay::~LCDDisplay() {
this->close();
}
int LCDDisplay::Init() {
if (const int ret = this->open(); ret < 0) {
this->close();
return -1;
}
struct termios term{};
// Setup the serial port 8 data bits, no parity, 1 stopbit, no flow control
term.c_cflag = baud | CS8 | CSTOPB | CLOCAL | CREAD;
term.c_iflag = 0;
term.c_oflag = 0;
term.c_lflag = 0;
tcflush(lcd, TCIFLUSH);
tcsetattr(lcd, TCSANOW, &term);
// init linebuf to spaces
this->clearBuf();
// disable underline and block cursor
constexpr uint8_t cmdbuf[] = {254, 75, 254, 84};
::write(lcd, &cmdbuf, sizeof(cmdbuf));
// clear display and set cursor home
this->clear();
return 0;
}
int LCDDisplay::open(const bool reopen) {
applog(LOG::INF, "Opening serial port at \"{}\"", dev);
if (lcd < 0 || reopen)
lcd = ::open(dev.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK); // ::open calls the global open() instead of LCDDisplay::open()
if (lcd < 0) {
applog(LOG::ERR, "Error opening serial port \"{}\"", dev); // err
return -1;
}
return 0;
}
void LCDDisplay::close() {
if (lcd >= 0) {
::close(lcd);
applog(LOG::INF, "LCD port closed");
}
lcd = -1;
}
void LCDDisplay::write(const uint8_t* cmdbuf, const uint8_t len) {
if (::write(lcd, cmdbuf, len) != len) {
applog(LOG::ERR, "failed to execute ::write, lost access to port?");
this->open(true);
}
}
void LCDDisplay::clear() {
constexpr uint8_t cmdbuf[] = {254, 88};
this->write(cmdbuf, sizeof(cmdbuf));
}
void LCDDisplay::setContrast(const uint8_t contrast) {
applog(LOG::INF, "Set contrast to {}", contrast);
const uint8_t cmdbuf[] = {254, 80, contrast};
this->write(cmdbuf, sizeof(cmdbuf));
}
void LCDDisplay::setBacklight(const bool enabled) {
applog(LOG::INF, "Set backlight to {}", enabled);
const uint8_t cmdbuf[] = {254, static_cast<uint8_t>(enabled ? 66 : 70), 255};
this->write(cmdbuf, enabled ? 3 : 2);
}
/* still doesnt work? */
void LCDDisplay::setBacklight(const uint8_t brightness) {
applog(LOG::INF, "Set backlight brightness to {}", brightness);
const uint8_t cmdbuf[] = {254, static_cast<uint8_t>(brightness ? 66 : 70), 255, brightness}; // 3rd char is timer, 4th is brightness
this->write(cmdbuf, brightness ? 4 : 2);
}
void LCDDisplay::setCursor(const uint8_t row, const uint8_t col) {
const uint8_t cmdbuf[] = {254, 71, static_cast<uint8_t>(col + 1), static_cast<uint8_t>(row + 1)};
this->write(cmdbuf, sizeof(cmdbuf));
}
/* reconsider so im not writing every time. prepend custom chars to the write buffer? */
void LCDDisplay::setCustomChar(const uint8_t index, const std::array<uint8_t, 8> cchar) {
uint8_t cmdbuf[] = {254, 78, index, 0, 0, 0, 0, 0, 0, 0, 0};
std::memcpy(&cmdbuf[3], &cchar, sizeof(cchar));
this->write(cmdbuf, sizeof(cmdbuf));
}
/* immediately writes string to the lcd at current cursor pos */
void LCDDisplay::writeStr(const std::string& str) {
this->write(reinterpret_cast<const uint8_t*>(str.c_str()), str.size());
}
/* immediately writes string to the lcd at given cursor pos */
void LCDDisplay::writeStr(const uint8_t row, const uint8_t col, const std::string& str) {
this->setCursor(row, col);
this->write(reinterpret_cast<const uint8_t*>(str.c_str()), str.size());
}
/* writes string to write buffer at calculated pos */
void LCDDisplay::writeBuf(const uint8_t row, const uint8_t col, const std::string& str) {
const size_t len = std::min(str.size(), writebuf.size() - col);
uint8_t pos = 4 + col + (LCD_COLS * (row % 2)); // skip 4 bytes to leave room for set cursor command
if (row >= 2) pos += 2 * LCD_COLS + 4; // if we're writing to disp2, skip another 4 bytes for set cursor command and 2*40 bytes of disp 1
std::memcpy(&writebuf[pos], str.c_str(), len);
}
/* initializes the write buffer to spaces */
void LCDDisplay::clearBuf() {
std::memset(writebuf.data(), ' ', writebuf.size());
}
/* flushes writebuffer to lcd, hyperoptmized, flushes buffer and commands in a single write operation */
void LCDDisplay::flushBuf() {
constexpr uint8_t setDisp[] = {254, 71, 1, 1}; // set cursor to char 1, line 1
std::memcpy(&writebuf[0], setDisp, 4);
std::memcpy(&writebuf[2 * LCD_COLS + 4], setDisp, 4);
writebuf[2 * LCD_COLS + 7] = 3; // overwrite set cursor c1 l1 to c1 l3
this->write(reinterpret_cast<uint8_t*>(writebuf.data()), writebuf.size());
}
// i have a whole flushbuf func to avoid too many writes, but im writing on every bar, need to rethink this.
// we could have a separate pre-char buffer and put all the setCustomChar commands to that?
char LCDDisplay::bar(const uint8_t char_id, const double value) {
std::array<uint8_t, 8> b{};
b.fill(0b01110);
for (uint8_t i = 0; i < (1.0 - value) * 8; i++)
b[i] = 0;
this->setCustomChar(char_id, b);
return cc(char_id);
}

View File

@@ -1,209 +0,0 @@
#include <LCDDisplay.hpp>
#include <app.hpp>
#include <chrono>
#include <cpu.hpp>
#include <csignal>
#include <functional>
#include <gpu.hpp>
#include <hass.hpp>
#include <log.hpp>
#include <mutex>
#include <stop_token>
#include <string>
#include <sys.hpp>
#include <thread>
#include <utility>
#include <utils.hpp>
#include <vector>
Application::Application(Config config) {
m_config = std::move(config);
// set up signals
sigemptyset(&m_control.sigset);
sigaddset(&m_control.sigset, SIGINT);
sigaddset(&m_control.sigset, SIGTERM);
applog(LOG::START, "Application initialized");
}
Application::~Application() {
auto level = LOG::START;
if (m_ret < 0) level = LOG::WRN;
applog(level, "Application destroyed");
}
int Application::Run() {
int sig;
m_mainPid = getpid();
pthread_sigmask(SIG_BLOCK, &m_control.sigset, nullptr);
{
std::vector<std::jthread> threads;
std::function<void(std::stop_token)> funcs[] = {
[this](const std::stop_token& st) { displayThread(st); },
[this](const std::stop_token& st) { sensorsThread(st); },
[this](const std::stop_token& st) { hassThread(st); },
};
// start threads in order
for (auto& f : funcs) {
threads.emplace_back(f);
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // start threads with delay so displayThread can quit the app if theres no lcd
}
// waits for signal, the threads are stopped when we go out of scope
sigwait(&m_control.sigset, &sig);
applog(LOG::WRN, "got stop signal, shutting down...");
}
return m_ret;
}
void Application::Crash(const int ret) {
m_ret = ret;
kill(m_mainPid, SIGINT);
}
void Application::hassThread(const std::stop_token& st) { //NOLINT
applog(LOG::START, "Thread started");
#ifdef ENABLE_HOME_ASSISTANT
static double power;
HomeAssistant::Config config;
config.address = m_config.hass.url;
config.entity = m_config.hass.entity;
config.token = m_config.hass.token;
HomeAssistant pc_power(config);
pc_power.connect();
while (!st.stop_requested()) {
pc_power.service();
if (power != pc_power.state) {
power = pc_power.state;
{
std::lock_guard lock(m_control.sensors);
m_sensors.hass_state = power;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
#else
(void)st;
applog(LOG::WRN, "feature disabled at compile time, probably no libwebsockets on host. exiting thread");
{
std::lock_guard lock(m_control.sensors);
m_sensors.hass_state = -1.0;
}
#endif
}
void Application::sensorsThread(const std::stop_token& st) {
applog(LOG::START, "Thread started, poll interval {} ms", m_config.lcd.refresh_interval_ms);
int ret = cpu::detect(&m_sensors.cpu, m_config.cpu_pciaddr);
ret += gpu::detect(&m_sensors.gpu, m_config.gpu_pciaddr);
ret += sys::detect(&m_sensors.sys, m_config.sys_sensor);
m_sensors.home.mountpoint = m_config.home_dir;
if (ret < 0)
applog(LOG::ERR, "Failed to detect some sensors");
while (!st.stop_requested()) {
auto start = std::chrono::steady_clock::now();
{
std::lock_guard lock(m_control.sensors);
sys::getTimeDate(&m_sensors.td);
getloadavg(m_sensors.loadavg, 3);
sys::getRam(&m_sensors.ram);
sys::getDisk(&m_sensors.home);
sys::getNetwork(&m_sensors.net, m_config.sys_ifname);
sys::updateSensors(&m_sensors.sys);
cpu::update(&m_sensors.cpu);
gpu::update(&m_sensors.gpu);
}
// log(LOG::DBG, "took {} microseconds", std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count());
m_control.display.release(); // unlocks display loop
std::this_thread::sleep_until(start + std::chrono::milliseconds(m_config.lcd.refresh_interval_ms));
}
applog(LOG::INF, "Thread stopped");
}
void Application::displayThread(const std::stop_token& st) {
std::stop_callback cb(st, [&] { m_control.display.release(); });
applog(LOG::START, "Thread started");
// init internal variables of LCDDisplay class
LCDDisplay lcd(m_config.lcd.port);
if (const int ret = lcd.Init(); ret < 0) {
applog(LOG::ERR, "Failed to initialize LCDDisplay");
this->Crash(ret);
return;
}
// basic lcd setup
lcd.setContrast(m_config.lcd.contrast);
lcd.setBacklight(true);
lcd.setCustomChar(0, {16, 6, 9, 8, 8, 9, 6, 0}); // celcius
lcd.setCustomChar(1, {0, 0, 4, 10, 10, 17, 31, 0}); // delta
lcd.setCustomChar(2, {0, 0, 8, 21, 21, 2, 0, 0}); // home (tilde)
lcd.setCustomChar(3, {4, 14, 21, 4, 4, 4, 0}); // up arrow
lcd.setCustomChar(4, {4, 4, 4, 21, 14, 4, 0}); // down arrow
// welcome msg
lcd.writeBuf(2, 0, utils::center("Waiting for sensors...", 40));
lcd.flushBuf();
lcd.clearBuf();
// actual main display loop
while (!st.stop_requested()) {
m_control.display.try_acquire_for(std::chrono::milliseconds(static_cast<int>(m_config.lcd.refresh_interval_ms * 1.25))); // holds the loop until sensor data is ready, max 1.25x of the loop time
if (st.stop_requested()) break; // needed, otherwise it wont quit correctly
{
std::lock_guard lock(m_control.sensors);
// prepare some tricky sensors
std::string gpu_temps = std::to_string(m_sensors.gpu.temp_edge);
if (m_sensors.gpu.temp_hotspot) {
const auto delta = m_sensors.gpu.temp_hotspot - m_sensors.gpu.temp_edge;
gpu_temps.append("/" + std::to_string(m_sensors.gpu.temp_hotspot));
gpu_temps.insert(0, LCDDisplay::cc(1) + std::to_string(delta) + LCDDisplay::cc(0) + " ");
}
uint16_t cpu_fan = 0;
for (const auto& fan : m_sensors.sys.fan_speed)
cpu_fan += fan;
cpu_fan /= 2;
// write to display buffer
lcd.writeBuf(0, 0, utils::pad(m_sensors.td.date, 11, utils::just::left) + " " + m_sensors.td.time + lcd.bar(7, m_sensors.gpu.usage_core / 100.0));
lcd.writeBuf(0, 26, utils::pNum(m_sensors.loadavg[0], m_sensors.loadavg[0] > 10 ? 1 : 2));
lcd.writeBuf(0, 31, utils::pNum(m_sensors.loadavg[1], m_sensors.loadavg[1] > 10 ? 1 : 2));
lcd.writeBuf(0, 36, utils::pNum(m_sensors.loadavg[2], m_sensors.loadavg[2] > 10 ? 1 : 2));
lcd.writeBuf(1, 0, utils::pad(std::to_string(m_sensors.cpu.tctl) + "/" + std::to_string(m_sensors.cpu.tccd1) + LCDDisplay::cc(0), 8, utils::just::left));
lcd.writeBuf(1, 7, utils::pad(utils::pBytes(m_sensors.ram.used, true), 6, utils::just::left));
if (m_sensors.hass_state >= 0)
lcd.writeBuf(1, 13, utils::pad(utils::pNum(m_sensors.hass_state) + "W", 4));
lcd.writeBuf(1, 19, std::to_string(m_sensors.sys.t_sensor) + LCDDisplay::cc(0)); // t_sensor temp
lcd.writeBuf(1, 32 - gpu_temps.size(), utils::pad(utils::pBytes(m_sensors.gpu.vram_used), 6));
lcd.writeBuf(1, 39 - gpu_temps.size(), gpu_temps + LCDDisplay::cc(0));
lcd.writeBuf(2, 0, utils::pad(std::to_string(static_cast<int>(m_sensors.cpu.ptotal)) + "W", 8, utils::just::left));
lcd.writeBuf(2, m_sensors.cpu.ptotal < 100 ? 4 : 5, std::to_string(cpu_fan) + "R"); // avg fan speed
lcd.writeBuf(2, 12, LCDDisplay::cc(3) + utils::pBytes(m_sensors.net.upload));
lcd.writeBuf(2, 19, LCDDisplay::cc(4) + utils::pBytes(m_sensors.net.download));
lcd.writeBuf(2, 26, utils::pad(std::to_string(m_sensors.gpu.fan_speed) + "R", 5));
lcd.writeBuf(2, 31, utils::pad(std::to_string(m_sensors.gpu.power_avg) + "/" + std::to_string(m_sensors.gpu.power_cap) + "W", 9));
lcd.writeBuf(3, 0, std::to_string(m_sensors.cpu.avg_freq) + "MHz");
lcd.writeBuf(3, 14, utils::pad(LCDDisplay::cc(2) + utils::pBytes(m_sensors.home.usage.free), 6));
lcd.writeBuf(3, 21, utils::pad(std::to_string(m_sensors.gpu.clock_core) + "/" + std::to_string(m_sensors.gpu.clock_mem) + "MHz ", 13));
lcd.writeBuf(3, 34, utils::pad(utils::pNum(m_sensors.gpu.vcore, 3) + "V", 5));
}
lcd.flushBuf();
lcd.clearBuf();
}
// goodbye msg
lcd.writeBuf(2, 0, utils::center("Goodbye", 40));
lcd.flushBuf();
}

View File

@@ -1,72 +0,0 @@
#include <array>
#include <chrono>
#include <cpu.hpp>
#include <cstdint>
#include <log.hpp>
#include <string>
#include <thread>
#include <utils.hpp>
namespace cpu {
int detect(cpuSensors* target, const std::string& pci) {
const std::string path = "/sys/bus/pci/devices/" + pci + "/hwmon";
try {
target->hwmon = utils::getFirstDir(path);
} catch (...) {
applog(LOG::ERR, "NO CPU HWMON KERNEL MODULE DETECTED");
return -1;
}
target->module = utils::fileExists(target->hwmon + "/in1_input") ? zenpower : k10temp; // idk if this is reliable, we're trying to avoid just reading the name
target->num_cores = std::thread::hardware_concurrency();
applog(LOG::INF, "Detected {} cores, module enum #{} in path \"{}\"", target->num_cores, static_cast<uint8_t>(target->module), target->hwmon);
return 0;
}
void update(cpuSensors* c) {
static std::array<char, 32> buf;
struct EnergyTs {
uint64_t energy = 0;
std::chrono::time_point<std::chrono::steady_clock> ts;
};
static EnergyTs prev;
EnergyTs curr;
// read power from intel-rapl? works on amd too, btop uses this trick, seems accurate
// it tracks with "socket power" in hwinfo on windows, its actually the same thing i had in lcdsmartie
curr.ts = std::chrono::steady_clock::now();
utils::fastReadNum("/sys/class/powercap/intel-rapl:0/energy_uj", buf, curr.energy);
c->ptotal = static_cast<float>(curr.energy - prev.energy) / static_cast<float>(std::chrono::duration_cast<std::chrono::microseconds>(curr.ts - prev.ts).count());
prev = curr;
// lots of reads but still faster than reading /proc/cpuinfo
c->avg_freq = 0;
for (uint8_t i = 0; i < c->num_cores; i++) {
uint32_t t = 0;
utils::fastReadNum<1000>("/sys/devices/system/cpu/cpu" + std::to_string(i) + "/cpufreq/cpuinfo_avg_freq", buf, t);
c->avg_freq += t;
}
c->avg_freq /= (c->num_cores > 1) ? c->num_cores : 1;
if (c->hwmon.empty())
return;
// commented out unused values so its faster
if (c->module == zenpower) {
utils::fastReadNum<1000>(c->hwmon + "/temp1_input", buf, c->tdie);
utils::fastReadNum<1000>(c->hwmon + "/temp2_input", buf, c->tctl);
utils::fastReadNum<1000>(c->hwmon + "/temp3_input", buf, c->tccd1);
// utils::fastReadNum<1000>(c->hwmon + "/in1_input", buf, c->vcore);
// utils::fastReadNum<1000>(c->hwmon + "/in2_input", buf, c->vsoc);
}
if (c->module == k10temp) {
utils::fastReadNum<1000>(c->hwmon + "/temp1_input", buf, c->tctl);
utils::fastReadNum<1000>(c->hwmon + "/temp3_input", buf, c->tccd1);
}
}
} // namespace cpu

View File

@@ -1,123 +0,0 @@
#include <array>
#include <gpu.hpp>
#include <gpu_metrics.hpp>
#include <log.hpp>
#include <string>
#include <utils.hpp>
/* maybe have a read pp_table function */
/* would be neat to wrap it in a class but eh namespace is good enough */
namespace gpu {
const std::string rel_metrics_path = "/../../gpu_metrics"; // gpu_metrics path relative to hwmon path
constexpr uint16_t max_metrics_length = 512; // length of the metrics buffer
/* takes pointer to a target gpu::sensors struct and updates its hwmon path and detects gpu_metrics */
int detect(gpuSensors* target, const std::string& pci) {
try {
target->hwmon = utils::getFirstDir("/sys/bus/pci/devices/" + pci + "/hwmon");
} catch (...) {
applog(LOG::ERR, "Couldn't find GPU hwmon");
return -1;
}
applog(LOG::INF, "Detected path \"{}\"", target->hwmon);
if (utils::fileExists(target->hwmon + rel_metrics_path)) {
std::array<char, max_metrics_length> raw_metrics{};
if (!utils::readFile(target->hwmon + rel_metrics_path, raw_metrics))
return -1;
const auto header = reinterpret_cast<metrics::metrics_table_header*>(raw_metrics.data());
applog(LOG::INF, "Found gpu_metrics_v{}_{}", header->format_revision, header->content_revision);
target->hasGpuMetrics = true;
}
return 0;
}
/* some metrics are missing in gpu_metrics, we always fill them from fs, unused metrics are commented out */
void fillMissingMetrics(gpuSensors* g) {
static std::array<char, 32> buf;
utils::fastReadNum<1000>(g->hwmon + "/in0_input", buf, g->vcore);
// g->fan_pwm = std::stoi(readFile(g->hwmon + "/pwm1", 4)) * 100 / 256;
utils::fastReadNum(g->hwmon + "/fan1_input", buf, g->fan_speed);
utils::fastReadNum<1000000>(g->hwmon + "/power1_cap", buf, g->power_cap);
// utils::fastReadNum(g->hwmon + "/power1_cap_max", g->power_cap_max) / 1000000;
utils::fastReadNum(g->hwmon + "/../../mem_info_vram_used", buf, g->vram_used);
// utils::fastReadNum(g->hwmon + "/../../mem_info_vram_total", g->vram_total);
}
/* get sensors for pre-vega cards (rx480/580) */
void updateFromFs(gpuSensors* g) {
static std::array<char, 32> buf;
utils::fastReadNum<1000000>(g->hwmon + "/freq1_input", buf, g->clock_core);
utils::fastReadNum<1000000>(g->hwmon + "/freq2_input", buf, g->clock_mem);
utils::fastReadNum<1000000>(g->hwmon + "/power1_input", buf, g->power_avg);
utils::fastReadNum<1000>(g->hwmon + "/temp1_input", buf, g->temp_edge);
utils::fastReadNum(g->hwmon + "/../../gpu_busy_percent", buf, g->usage_core);
}
/* get sensors for vega and newer (rx5700xt, rx7800xt [*]) */
void updateFromMetrics(gpuSensors* g) {
std::array<char, max_metrics_length> raw_metrics{};
if (!utils::readFile(g->hwmon + rel_metrics_path, raw_metrics)) return;
switch (const auto header = reinterpret_cast<metrics::metrics_table_header*>(raw_metrics.data()); header->format_revision * 10 + header->content_revision) {
/* rx5700xt, rx7800xt */
case 13: {
const auto metrics = reinterpret_cast<metrics::gpu_metrics_v1_3*>(raw_metrics.data());
g->usage_core = metrics->average_gfx_activity;
g->usage_umc = metrics->average_umc_activity;
g->usage_media = metrics->average_mm_activity;
g->temp_edge = metrics->temperature_edge;
g->temp_hotspot = metrics->temperature_hotspot;
g->temp_mem = metrics->temperature_mem;
g->temp_vrm_gfx = metrics->temperature_vrgfx;
g->temp_vrm_soc = metrics->temperature_vrsoc;
g->temp_vrm_mem = metrics->temperature_vrmem;
g->power_avg = metrics->average_socket_power;
g->fan_speed = metrics->current_fan_speed;
// hacks for rx7800xt
// g->clock_core = metrics->average_vclk1_frequency; // why not gfxclock?
// g->clock_mem = metrics->average_gfxclk_frequency; // why gfxclock? min mem clock seems to be 96mhz, this is probably including time sleeping
// hacks for rx5700xt
g->clock_core = metrics->average_gfxclk_frequency;
g->clock_mem = metrics->average_uclk_frequency * 2; // mem controller clock, actual mem is 2x that
break;
}
/* ryzen 5 5600g */
case 22: {
const auto metrics = reinterpret_cast<metrics::gpu_metrics_v2_2*>(raw_metrics.data());
g->temp_edge = metrics->temperature_gfx / 100;
break;
}
/* werid hardware i probably dont like, maybe fallback to reading from fs? */
default: {
applog(LOG::WRN, "Unsupported gpu_metrics_v{}_{}", header->format_revision, header->content_revision);
break;
}
}
}
/* reads values from system and updates the target gpu::sensors struct */
void update(gpuSensors* g) {
const bool hwmon_valid = !g->hwmon.empty();
if (hwmon_valid)
fillMissingMetrics(g);
if (g->hasGpuMetrics)
updateFromMetrics(g);
else
if (hwmon_valid)
updateFromFs(g);
if (g->fan_speed > 9999) g->fan_speed = 9999; // sanity check fan speed
}
} // namespace gpu

View File

@@ -1,163 +0,0 @@
#ifdef ENABLE_HOME_ASSISTANT
#include <libwebsockets.h>
#include <cstring>
#include <hass.hpp>
#include <log.hpp>
#include <nlohmann/json.hpp>
#include <string>
HomeAssistant::HomeAssistant(const Config& config) {
m_config = config;
}
HomeAssistant::~HomeAssistant() {
disconnect();
}
void HomeAssistant::event_handler(lws* wsi, const lws_callback_reasons reason, char* in) {
switch (reason) {
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {
applog(LOG::ERR, "websocket conn error: {}", in ? in : "(null)");
return;
}
case LWS_CALLBACK_CLIENT_RECEIVE: {
nlohmann::json json = nlohmann::json::parse(in);
const std::string ha_type = json["type"].get<std::string>();
if (ha_type == "auth_required") {
json.clear();
json["type"] = "auth";
json["access_token"] = m_config.token;
tx = json.dump();
break;
}
if (ha_type == "auth_ok") {
applog(LOG::INF, "subscribing to entity \"{}\"", m_config.entity);
json.clear();
json["id"] = id++;
json["type"] = "subscribe_entities";
json["entity_ids"][0] = m_config.entity.c_str();
tx = json.dump();
break;
}
if (ha_type == "result") {
if (json["success"].get<bool>() != true)
applog(LOG::ERR, "api said success was \"false\" on message id {}", id - 1);
break;
}
if (ha_type == "event") {
json = json["event"];
if (json.contains("a"))
state = std::stof(json["a"][m_config.entity]["s"].get<std::string>());
if (json.contains("c"))
state = std::stof(json["c"][m_config.entity]["+"]["s"].get<std::string>());
break;
}
applog(LOG::WRN, "unhandled event: {}", static_cast<char*>(in));
break;
}
case LWS_CALLBACK_CLIENT_ESTABLISHED: {
applog(LOG::INF, "Websocket connected");
break;
}
case LWS_CALLBACK_CLIENT_CLOSED: {
applog(LOG::INF, "Websocket closed");
return;
}
case LWS_CALLBACK_CLIENT_WRITEABLE: {
/* skip whole callback if there's no tx to send */
if (!tx.empty()) {
const size_t msgLen = LWS_PRE + tx.size();
auto* buf = static_cast<unsigned char*>(malloc(msgLen));
if (buf) {
memcpy(&buf[LWS_PRE], tx.c_str(), tx.size());
lws_write(wsi, &buf[LWS_PRE], tx.size(), LWS_WRITE_TEXT);
tx = "";
free(buf);
}
}
break;
}
default:
break;
}
}
int HomeAssistant::callback(lws* wsi, const lws_callback_reasons reason, void* user, void* in, size_t) {
auto* self = static_cast<HomeAssistant*>(user);
self->event_handler(wsi, reason, static_cast<char*>(in));
return 0;
}
int HomeAssistant::connect() {
lws_client_connect_info i{};
lws_context_creation_info info{};
lws_set_log_level(LLL_ERR | LLL_WARN, lwsl_emit_syslog);
std::memset(&i, 0, sizeof(i));
std::memset(&info, 0, sizeof info);
static constexpr lws_protocols protocols[] = {
{"lws-minimal-client", callback, sizeof(void*), 0, 0, nullptr, 0},
LWS_PROTOCOL_LIST_TERM};
static constexpr lws_extension extensions[] = {
{"permessage-deflate",
lws_extension_callback_pm_deflate,
"permessage-deflate"
"; client_no_context_takeover"
"; client_max_window_bits"},
{nullptr, nullptr, nullptr}};
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.port = CONTEXT_PORT_NO_LISTEN;
info.protocols = protocols;
info.fd_limit_per_thread = 1 + 1 + 1;
info.extensions = extensions;
context = lws_create_context(&info);
if (!context) {
applog(LOG::ERR, "error creating context");
return -1;
}
i.context = context;
i.port = 443;
i.address = m_config.address.c_str();
i.path = "/api/websocket";
i.host = i.address;
i.origin = i.address;
i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_PRIORITIZE_READS;
i.protocol = nullptr;
i.userdata = this;
i.local_protocol_name = "lws-minimal-client";
websock = lws_client_connect_via_info(&i);
if (!websock) {
applog(LOG::ERR, "error creating lws client");
return -1;
}
return 0;
}
void HomeAssistant::service(const int timeout_ms) const {
lws_callback_on_writable(websock);
lws_service(context, timeout_ms);
}
void HomeAssistant::disconnect() const {
lws_context_destroy(context);
}
#endif

133
src/lcd.c Normal file
View File

@@ -0,0 +1,133 @@
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <string.h>
#include "log.h"
#include "lcd.h"
#define MIN(a,b) ((a) < (b) ? (a) : (b))
int lcd_open(struct lcd_t* lcd, const bool reopen) {
applog(LOG_INF, "Opening serial port at %s", lcd->port);
if (lcd->fd <= 0 || reopen)
lcd->fd = open(lcd->port, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (lcd->fd <= 0) {
applog(LOG_ERR, "Error opening serial port %s", lcd->port); // err
return -1;
}
return 0;
}
void lcd_close(struct lcd_t* lcd) {
if (lcd->fd >= 0) {
close(lcd->fd);
applog(LOG_INF, "LCD port closed");
}
lcd->fd = -1;
}
void lcd_write(struct lcd_t* lcd, const uint8_t* cmdbuf, const uint8_t len) {
if (!write(lcd->fd, cmdbuf, len)) {
applog(LOG_ERR, "failed to execute write(), lost access to port?");
lcd_open(lcd, true);
}
}
void lcd_clear(struct lcd_t* lcd) {
constexpr uint8_t cmdbuf[] = {254, 88};
lcd_write(lcd, cmdbuf, sizeof(cmdbuf));
}
void lcd_set_contrast(struct lcd_t* lcd, const uint8_t contrast) {
applog(LOG_INF, "Set contrast to %d", contrast);
const uint8_t cmdbuf[] = {254, 80, contrast};
lcd_write(lcd, cmdbuf, sizeof(cmdbuf));
}
void lcd_set_backlight(struct lcd_t* lcd, const bool enabled) {
applog(LOG_INF, "Set backlight to %s", enabled ? "true" : "false");
const uint8_t cmdbuf[] = {254, (enabled ? 66 : 70), 255};
lcd_write(lcd, cmdbuf, enabled ? 3 : 2);
}
void lcd_set_cursor(struct lcd_t* lcd, const uint8_t row, const uint8_t col) {
const uint8_t cmdbuf[] = {254, 71, (col + 1), (row + 1)};
lcd_write(lcd, cmdbuf, sizeof(cmdbuf));
}
/* reconsider so im not writing every time. prepend custom chars to the write buffer? */
void lcd_set_custom_char(struct lcd_t* lcd, const uint8_t index, const lcd_glyph_t* glyph) {
uint8_t cmdbuf[] = {254, 78, index, 0, 0, 0, 0, 0, 0, 0, 0};
memcpy(&cmdbuf[3], &glyph, sizeof(glyph));
lcd_write(lcd, cmdbuf, sizeof(cmdbuf));
}
/* immediately writes string to the lcd at current cursor pos */
void lcd_write_str(struct lcd_t* lcd, const char* str, const uint8_t len) {
lcd_write(lcd, (const uint8_t*)str, len);
}
void lcd_write_str_pos(struct lcd_t* lcd, const char* str, const uint8_t len, const uint8_t posX, const uint8_t posY) {
lcd_set_cursor(lcd, posX, posY);
lcd_write(lcd, (const uint8_t*)str, len);
}
/* writes string to write buffer at calculated pos, we trust it to be null terminated */
void lcd_write_buf(struct lcd_t* lcd, const uint8_t row, const uint8_t col, const char* str) {
size_t len = 0;
while (str[len] != '\0')
len++;
len = MIN(len, sizeof(lcd->writebuf) - col);
uint8_t pos = 4 + col + (LCD_COLS * (row % 2)); // skip 4 bytes to leave room for set cursor command
if (row >= 2) pos += 2 * LCD_COLS + 4; // if we're writing to disp2, skip another 4 bytes for set cursor command and 2*40 bytes of disp 1
memcpy(&lcd->writebuf[pos], str, len);
}
void lcd_clear_buf(struct lcd_t* lcd) {
memset(lcd->writebuf, ' ', sizeof(lcd->writebuf));
}
/* flushes writebuffer to lcd, hyperoptmized, flushes buffer and commands in a single write operation */
void lcd_flush_buf(struct lcd_t* lcd) {
constexpr uint8_t set_disp[] = {254, 71, 1, 1}; // set cursor to char 1, line 1
memcpy(&lcd->writebuf[0], set_disp, 4);
memcpy(&lcd->writebuf[2 * LCD_COLS + 4], set_disp, 4);
lcd->writebuf[2 * LCD_COLS + 7] = 3; // overwrite set cursor c1 l1 to c1 l3
lcd_write(lcd, (const uint8_t*)lcd->writebuf, sizeof(lcd->writebuf));
}
// i have a whole flushbuf func to avoid too many writes, but im writing on every bar, need to rethink this.
// we could have a separate pre-char buffer and put all the setCustomChar commands to that?
char lcd_bar(struct lcd_t* lcd, const uint8_t char_id, const double value) {
uint8_t b[8];
memset(b, 0b01110, sizeof(b));
for (uint8_t i = 0; i < (1.0 - value) * 8; i++)
b[i] = 0;
lcd_set_custom_char(lcd, char_id, &b);
return cc(char_id);
}
int lcd_init(struct lcd_t* lcd) {
const int ret = lcd_open(lcd, false);
if (ret < 0) {
lcd_close(lcd);
return -1;
}
applog(LOG_INF, "ret %d, fd %d", ret, lcd->fd);
struct termios term = {};
term.c_cflag = lcd->baud | CS8 | CLOCAL | CREAD;
tcflush(lcd->fd, TCIFLUSH);
tcsetattr(lcd->fd, TCSANOW, &term);
lcd_clear_buf(lcd);
constexpr uint8_t cmdbuf[] = {254, 75, 254, 84};
lcd_write(lcd, cmdbuf, sizeof(cmdbuf));
lcd_clear(lcd);
return 0;
}

95
src/log.c Normal file
View File

@@ -0,0 +1,95 @@
#include "log.h"
#ifdef APPLOG_ENABLED
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#ifdef LOG_FILE_LOCATION
#include <zlib.h>
static pthread_mutex_t fmtx = PTHREAD_MUTEX_INITIALIZER;
#endif
static pthread_mutex_t cmtx = PTHREAD_MUTEX_INITIALIZER;
/* Replicates pFname: strips return type and parameters from __PRETTY_FUNCTION__
* "void myFunc(int x)" → "myFunc"
* "int other_func(void)" → "other_func"
*/
static void fmt_funcname(const char *pretty, char *out, size_t outsz) {
const char *start = pretty;
const char *space = strchr(pretty, ' ');
if (space) start = space + 1;
const char *paren = strrchr(start, '(');
size_t len = paren ? (size_t)(paren - start) : strlen(start);
if (len >= outsz) len = outsz - 1;
memcpy(out, start, len);
out[len] = '\0';
}
void log_write(const LogLevel level, const char *func, const char *fmt, ...) {
static const char *reset = "\033[0m";
const char *color;
switch (level) {
case LOG_INF: color = "\033[1;32m"; break; /* bold green */
case LOG_WRN: color = "\033[1;33m"; break; /* bold yellow */
case LOG_ERR: color = "\033[1;31m"; break; /* bold red */
case LOG_DBG: color = ""; break;
case LOG_START: color = "\033[1;3;36m"; break; /* bold italic cyan */
default: color = ""; break;
}
char funcname[128];
fmt_funcname(func, funcname, sizeof(funcname));
/* ── console ─────────────────────────────────────────────────────── */
{
va_list ap;
va_start(ap, fmt);
pthread_mutex_lock(&cmtx);
fprintf(stderr, "[%s%s%s] ", color, funcname, reset);
vfprintf(stderr, fmt, ap);
fputc('\n', stderr);
pthread_mutex_unlock(&cmtx);
va_end(ap);
}
#ifdef LOG_FILE_LOCATION
/* ── file ────────────────────────────────────────────────────────── */
{
char timebuf[32];
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
struct tm tm_info;
localtime_r(&ts.tv_sec, &tm_info);
strftime(timebuf, sizeof(timebuf), "%x %H:%M:%S", &tm_info);
char msgbuf[1024];
va_list ap;
va_start(ap, fmt);
int offset = snprintf(msgbuf, sizeof(msgbuf),
"%s.%03ld [%s%s%s] ",
timebuf, ts.tv_nsec / 1000000L,
color, funcname, reset);
vsnprintf(msgbuf + offset, sizeof(msgbuf) - offset, fmt, ap);
va_end(ap);
pthread_mutex_lock(&fmtx);
gzFile file = gzopen(LOG_FILE_LOCATION, "ab");
if (file) {
gzprintf(file, "%s\n", msgbuf);
gzclose(file);
}
pthread_mutex_unlock(&fmtx);
}
#endif /* LOG_FILE_LOCATION */
}
#endif /* APPLOG_ENABLED */

115
src/main.c Normal file
View File

@@ -0,0 +1,115 @@
#include <signal.h>
#include <pthread.h>
#include <stdlib.h>
#include "log.h"
#include <unistd.h>
#include <stdatomic.h>
#include <sys/signalfd.h>
#include "lcd.h"
atomic_bool g_running; // if using c11, you need atomic_store() and atomic_load();
struct control_t {
pthread_mutex_t sensors_mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t sensors_ready = PTHREAD_COND_INITIALIZER,
} control;
struct sensors_t {
// sys::TimeDate td;
double loadavg[3];
// sys::Memory ram;
// sys::Disk home;
// sys::Network net;
// sys::sysSensors sys;
// cpu::cpuSensors cpu;
// gpu::gpuSensors gpu;
double hass_state;
};
static void *display_thread(void *arg) {
applog(LOG_START, "display thread started");
struct sensors_t* sensors = arg;
sensors->hass_state = 1;
struct lcd_t lcd = {
.port = "/dev/serial/by-id/usb-Raspberry_Pi_Pico_E6616407E37AA12F-if00",
.baud = 0010002, // from termios-baud.h, B115200
};
lcd_init(&lcd);
// basic lcd setup
lcd_set_contrast(&lcd, 215);
lcd_set_backlight(&lcd, true);
lcd_set_custom_char(&lcd, 0, &(lcd_glyph_t){16, 6, 9, 8, 8, 9, 6, 0}); // celcius
lcd_set_custom_char(&lcd, 1, &(lcd_glyph_t){0, 0, 4, 10, 10, 17, 31, 0}); // delta
lcd_set_custom_char(&lcd, 2, &(lcd_glyph_t){0, 0, 8, 21, 21, 2, 0, 0}); // home (tilde)
lcd_set_custom_char(&lcd, 3, &(lcd_glyph_t){4, 14, 21, 4, 4, 4, 0}); // up arrow
lcd_set_custom_char(&lcd, 4, &(lcd_glyph_t){4, 4, 4, 21, 14, 4, 0}); // down arrow
lcd_write_buf(&lcd, 2, 0, "Waiting for sensors..."); // need to center this still
lcd_flush_buf(&lcd);
lcd_clear_buf(&lcd);
while (g_running) { // NOLINT its updated globally bud
;
}
lcd_close(&lcd);
applog(LOG_WRN, "display thread stopped");
return nullptr;
}
static void *sensors_thread(void *arg) {
applog(LOG_START, "sensors thread started");
struct sensors_t* sensors = arg;
(void)sensors;
applog(LOG_WRN, "sensors thread stopped");
return nullptr;
}
int main(void) {
g_running = true;
applog(LOG_START, "linuxSmartie started");
/* signal setup */
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGINT);
sigaddset(&sigset, SIGTERM);
sigaddset(&sigset, SIGQUIT);
pthread_sigmask(SIG_BLOCK, &sigset, nullptr);
struct signalfd_siginfo info;
const int sfd = signalfd(-1, &sigset, SFD_CLOEXEC);
struct sensors_t *sensors = malloc(sizeof(struct sensors_t));
if (sensors == nullptr) {
applog(LOG_ERR, "cant allocate mem for the sensor struct");
free(sensors);
return -1;
}
applog(LOG_INF, "sensor struct allocated, size is %lu bytes", sizeof(struct sensors_t));
applog(LOG_INF, "sensors is %.1f", sensors->hass_state);
pthread_t display_pthread;
pthread_t sensors_pthread;
pthread_create(&display_pthread, nullptr, display_thread, sensors);
pthread_create(&sensors_pthread, nullptr, sensors_thread, sensors);
read(sfd, &info, sizeof(info)); // halts on this until SIGTERM
g_running = false;
close(sfd);
applog(LOG_WRN, "got stop signal, shutting down...");
pthread_join(display_pthread, nullptr);
pthread_join(sensors_pthread, nullptr);
applog(LOG_INF, "sensors is %.1f", sensors->hass_state);
free(sensors);
applog(LOG_INF, "freed memory");
return 0;
}

View File

@@ -1,87 +0,0 @@
#include <sys/statvfs.h>
#include <charconv>
#include <chrono>
#include <format>
#include <log.hpp>
#include <string>
#include <sys.hpp>
#include <utils.hpp>
int getVal(const std::string_view target, std::string_view content) {
int result = -1;
if (const std::size_t start = content.find(target); start != std::string::npos) {
content.remove_prefix(start + target.length());
const std::size_t end = content.find(" kB");
while (std::isspace(content.front()))
content.remove_prefix(1);
std::from_chars(content.data(), content.data() + end, result);
}
return result;
}
void sys::getTimeDate(TimeDate* td) {
const auto now = std::chrono::system_clock::now();
const auto local = std::chrono::current_zone()->to_local(now);
const std::chrono::year_month_day ymd{floor<std::chrono::days>(local)};
const std::chrono::weekday wd{floor<std::chrono::days>(local)};
td->time = std::format("{:%H:%M:%S}", floor<std::chrono::seconds>(local));
td->date = std::format("{}, {} {}", wdays[wd.c_encoding()], ymd.day(), months[static_cast<unsigned>(ymd.month()) - 1]);
}
void sys::getRam(Memory* mem) {
static std::array<char, 128> buf; // file is longer but we only need first 128 chars
utils::readFile("/proc/meminfo", buf);
mem->free = getVal("MemAvailable:", buf.data());
mem->total = getVal("MemTotal:", buf.data()); // shouldn't really update this on each loop
mem->used = mem->total - mem->free;
}
void sys::getDisk(Disk* disk) {
struct statvfs diskData{};
statvfs(disk->mountpoint.c_str(), &diskData);
disk->usage.free = diskData.f_bfree * diskData.f_frsize;
disk->usage.used = (diskData.f_blocks - diskData.f_bfree) * diskData.f_frsize;
disk->usage.total = diskData.f_blocks * diskData.f_frsize;
}
void sys::getNetwork(Network* net, const std::string& device) {
struct Network {
std::chrono::time_point<std::chrono::steady_clock> ts;
uint64_t rx = 0;
uint64_t tx = 0;
};
static std::array<char, 32> buf;
static Network prev;
Network current;
current.ts = std::chrono::steady_clock::now();
utils::fastReadNum("/sys/class/net/" + device + "/statistics/rx_bytes", buf, current.rx);
utils::fastReadNum("/sys/class/net/" + device + "/statistics/tx_bytes", buf, current.tx);
const double ts_diff = static_cast<double>(std::chrono::duration_cast<std::chrono::microseconds>(current.ts - prev.ts).count()) / 1000000.0f;
net->download = static_cast<uint32_t>(static_cast<double>(current.rx - prev.rx) / ts_diff);
net->upload = static_cast<uint32_t>(static_cast<double>(current.tx - prev.tx) / ts_diff);
prev = current;
}
int sys::detect(sysSensors* s, const std::string& platform) {
// we dont really need getFirstDir since its a static folder, but its useful for try/catch
try {
s->hwmon = utils::getFirstDir("/sys/devices/platform/" + platform + "/hwmon");
} catch (...) {
applog(LOG::ERR, "Can't open \"/sys/devices/platform/{}/hwmon\". Kernel params added? Module loaded?", platform);
return -1;
}
applog(LOG::INF, "Found system sensors");
return 0;
}
void sys::updateSensors(sysSensors* s) {
if (s->hwmon.empty())
return;
static std::array<char, 8> buf;
utils::fastReadNum(s->hwmon + "/fan2_input", buf, s->fan_speed[0]);
utils::fastReadNum(s->hwmon + "/fan7_input", buf, s->fan_speed[1]);
utils::fastReadNum<1000>(s->hwmon + "/temp5_input", buf, s->t_sensor);
}

View File

@@ -1,32 +0,0 @@
#include <utils.hpp>
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
namespace utils {
/* pads given string to length */
std::string pad(std::string s, const uint8_t len, const just j) {
if (len > s.size())
s.insert(j == left ? s.end() : s.begin(), len - s.size(), fill_char);
return s;
}
/* returns the string centered within given length */
std::string center(std::string str, const uint8_t len) {
const uint8_t padding = len - str.size();
str.insert(str.begin(), padding / 2, fill_char);
str.insert(str.end(), padding / 2, fill_char);
return str;
}
/* finds first dir in path and returns it. don't refactor, you don't understand it, its fast enough, and you only use it on init */
std::string getFirstDir(const std::string_view& path) {
for (const auto& entry : std::filesystem::directory_iterator(path))
return entry.path();
return "";
}
} // namespace utils