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,4 +1,56 @@
---
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 0
BasedOnStyle: LLVM
# Indentation (Linux kernel style)
UseTab: Always
IndentWidth: 8
TabWidth: 8
# Keep parameters on one line as much as possible
BinPackParameters: true
BinPackArguments: true
# Allow long lines (important!)
ColumnLimit: 0 # or set to something large like 120
# Prevent breaking after return type / before function name
AlwaysBreakAfterReturnType: None
AlwaysBreakAfterDefinitionReturnType: None
# Keep function declarations/definitions on one line if possible
PenaltyBreakBeforeFirstCallParameter: 1000000
PenaltyReturnTypeOnItsOwnLine: 1000000
# Braces: K&R style everywhere, INCLUDING functions (your modification)
BreakBeforeBraces: Attach
# Control specific brace wrapping behavior
BraceWrapping:
AfterFunction: false # <-- key change (same line for functions)
AfterControlStatement: false
AfterStruct: false
AfterEnum: false
AfterUnion: false
AfterClass: false
BeforeElse: false
# Spaces
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
# Pointer alignment (kernel prefers this style)
PointerAlignment: Right
# Keep simple statements compact
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
# For-loop style similar to kernel
AllowShortBlocksOnASingleLine: Never
# Don't align weirdly
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
# Include handling (kernel is manual about includes)
SortIncludes: false

View File

@@ -1,8 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<RiderCodeStyleSettings>
<option name="/Default/CodeStyle/CodeFormatting/CppClangFormat/EnableClangFormatSupport/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue" value="false" type="bool" />
<option name="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_BINARY_EXPRESSIONS_CHAIN/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue" value="false" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_EXPRESSION/@EntryValue" value="false" type="bool" />
@@ -98,8 +97,5 @@
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/CONTINUOUS_LINE_INDENT/@EntryValue" value="Double" type="string" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/TAB_WIDTH/@EntryValue" value="4" type="int" />
</RiderCodeStyleSettings>
<clangFormatSettings>
<option name="ENABLED" value="true" />
</clangFormatSettings>
</code_scheme>
</component>

3
.idea/editor.xml generated
View File

@@ -245,7 +245,6 @@
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=IfStdIsConstantEvaluatedCanBeReplaced/@EntryIndexedValue" value="SUGGESTION" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=StdIsConstantEvaluatedWillAlwaysEvaluateToConstant/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeStyle/CodeFormatting/CppClangFormat/EnableClangFormatSupport/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_ARGUMENT/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_BINARY_EXPRESSIONS_CHAIN/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue" value="false" type="bool" />
@@ -340,6 +339,6 @@
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/WRAP_BEFORE_INVOCATION_RPAR/@EntryValue" value="false" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/WRAP_BEFORE_TERNARY_OPSIGNS/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/WRAP_PARAMETERS_STYLE/@EntryValue" value="WRAP_IF_LONG" type="string" />
<option name="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue" value="false" type="bool" />
<option name="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue" value="true" type="bool" />
</component>
</project>

View File

@@ -2,18 +2,17 @@ cmake_minimum_required(VERSION 3.20)
project(linuxSmartie
VERSION 1.0
LANGUAGES CXX
LANGUAGES C
)
# Use C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Use C23
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)
# Collect sources
file(GLOB SOURCES
src/*.cpp
main.cpp
src/*.c
)
# Define executable
@@ -27,12 +26,14 @@ target_include_directories(linuxSmartie
# Compiler warnings
target_compile_options(linuxSmartie
PRIVATE
PRIVATE
-O2
-Wall
-Wextra
-Wpedantic
-Werror
-Wshadow
-Wformat=2 # replaces what -Wpedantic caught for printf
-Wformat-security
-fdiagnostics-color=always
)
@@ -47,7 +48,7 @@ else()
endif()
if(ZLIB_FOUND)
target_link_libraries(linuxSmartie PRIVATE z)
target_link_libraries(linuxSmartie PRIVATE ZLIB::ZLIB)
target_compile_definitions(linuxSmartie PRIVATE ENABLE_ZLIB)
else()
message(STATUS "no zlib found, disabling saving logs to gz")

View File

@@ -1,27 +1,22 @@
# linuxSmartie
Alphanumeric LCD controller software for Linux (kind of LCDSmartie for Windows but no GUI, no config file, no plugins, no Windows).
~~Written with C++26 (`-std=c++2c`), will need at least gcc 14 to compile.~~ Uhm actually written with C++20 now, I was only really using `std::print` from 26. Let us not fall for memes. Will need gcc 10 to compile. GCC actually made C++20 the default since v16.
We can't go lower than C++20, we use too much shit introduced by it (std::format, std::chrono stuff mostly).
Big ol rewrite in C, like God intended because GOOD LORD C++ IS A MESS.
Hardcoded to work with 4x40 character display because that's what I have and I don't plan on using anything else. Uses Matrix Orbital protocol over serial. Should work with most other display sizes by modifying `src/LCDDisplay.cpp`. Best of luck if you try to make it work with anything else.
Vaguely inspired by suckless software philosophy, as in:
## The config is the source code
Same as plugins. Just write in more features scrub, learn some C++, damn
Same as plugins. Just write in more features scrub, learn some C, damn
## TODOs
- Use `std::string_view` or `const std::string &` when possible, its faster, no copy
- Extra screens support perhaps? just halt sensors monitoring thread and print another struct on display if needed, have a semaphore or sth
- Maybe not even screens, keep the 1st line and change lines 2-4
- Media playback screen (use mpris, get playback info with mpris)
- In the LCDDisplay lib, reimplement all the features I have in picoSmartie + maybe finally add keypad support for both so I can toggle backlight (?) or change screens if I want any
- Screens could be automatic tbh, like if it detects media playing it switches to media screen, if in media screen and GPU power > 80W it switches to main (hwmon) screen
- Try to be C++, avoid globals, wrap in namespaces, use naming conventions, use classes, avoid pure C way of doing things unless it makes more sense (which it often does tbh, better for reading small files)
- **FUCK C++, HUGE REWRITE FROM SCRATCH IN C LIKE GOD ALWAYS INTENDED** but fuuuuck display thread would be so painful without std::string. And I really would need to start a whole ass project from a clean slate since I made it a point to use a lot of C++ features which now bite me in the ass.
- Fuck it man, nvm let's leave it. Make it C++17 or 20 or sth so it compiles on yo mama or sth.
## LCD layout
It looks something like this

View File

@@ -1,48 +0,0 @@
#ifndef LCDDISPLAY_HPP
#define LCDDISPLAY_HPP
#define LCD_LINES 4
#define LCD_COLS 40
#include <array>
#include <cstdint>
#include <string>
class LCDDisplay {
private:
int lcd = -1;
uint32_t baud = 0;
std::string dev;
std::array<char, LCD_LINES * LCD_COLS + 8> writebuf{}; // 4x40 chars + 2x4 setcursor commands
static constexpr bool dualDisp = (LCD_LINES * LCD_COLS > 80); // is it dual display device or not, evauluated from the size at compile time
int open(bool reopen = false);
void write(const uint8_t* cmdbuf, uint8_t len);
void close();
// we avoid using these, use buffer (even if these are faster for small writes, perhaps a rewrite is in order)
void writeStr(const std::string& str);
void writeStr(uint8_t row, uint8_t col, const std::string& str);
public:
explicit LCDDisplay(const std::string& dev, uint32_t baud = 0010002); // from termios-baud.h, B115200
~LCDDisplay();
int Init();
void clear();
void setContrast(uint8_t contrast);
void setBacklight(bool enabled);
void setBacklight(uint8_t brightness);
void setCursor(uint8_t row, uint8_t col);
void setCustomChar(uint8_t index, std::array<uint8_t, 8> cchar);
void writeBuf(uint8_t row, uint8_t col, const std::string& str);
void clearBuf();
void flushBuf();
char bar(uint8_t char_id, double value);
// returns custom char no. c, small enough to define in header
static constexpr char cc(const uint8_t c) { return static_cast<char>(c % 8); }
};
#endif

View File

@@ -1,71 +0,0 @@
#ifndef APP_HPP
#define APP_HPP
#include <atomic>
#include <cpu.hpp>
#include <gpu.hpp>
#include <mutex>
#include <semaphore>
#include <stop_token>
#include <sys.hpp>
#include <thread>
class Application {
public:
struct Config {
struct Lcd {
uint16_t refresh_interval_ms;
uint8_t contrast;
std::string port;
};
struct HomeAssistant {
std::string url;
std::string entity;
std::string token;
};
Lcd lcd;
HomeAssistant hass;
std::string cpu_pciaddr;
std::string gpu_pciaddr;
std::string sys_sensor;
std::string sys_ifname;
std::string home_dir;
};
explicit Application(Config config);
~Application();
int Run();
private:
struct Sensors {
sys::TimeDate td;
double loadavg[3] = {0.0f};
sys::Memory ram;
sys::Disk home;
sys::Network net;
sys::sysSensors sys;
cpu::cpuSensors cpu;
gpu::gpuSensors gpu;
double hass_state = 0.0f;
};
struct Control {
std::binary_semaphore display{0};
std::mutex sensors; // making sure the sensors struct isnt accessed by multiple threads at once
sigset_t sigset;
};
pid_t m_mainPid{};
int m_ret = 0;
Config m_config{}; // m_ is prefix for member variable
Control m_control{};
Sensors m_sensors{};
void hassThread(const std::stop_token& st);
void sensorsThread(const std::stop_token& st);
void displayThread(const std::stop_token& st);
void Crash(int ret);
};
#endif

View File

@@ -1,33 +0,0 @@
#ifndef CPU_HPP
#define CPU_HPP
#include <cstdint>
#include <string>
namespace cpu {
enum Module : uint8_t { unknown,
k10temp,
zenpower };
struct cpuSensors {
Module module = unknown;
uint8_t num_cores = 0;
int16_t tdie = 0;
int16_t tctl = 0;
int16_t tccd1 = 0;
// double pcore = 0.0;
// double psoc = 0.0;
float ptotal = 0.0;
// double vcore = 0.0;
// double vsoc = 0.0;
uint32_t avg_freq = 0;
std::string hwmon;
};
int detect(cpuSensors* target, const std::string& pci);
void update(cpuSensors* c);
} // namespace cpu
#endif

View File

@@ -1,85 +0,0 @@
#ifndef GPU_HPP
#define GPU_HPP
#include <cstdint>
#include <string>
/* clean abstraction for gpu_metrics or metrics in filesystem in sane units */
namespace gpu {
struct gpuSensors {
uint64_t vram_used = 0;
// uint64_t vram_total = 0;
float vcore = 0.0f;
uint16_t clock_core = 0;
uint16_t clock_mem = 0;
// uint8_t fan_pwm = 0;
uint16_t fan_speed = 0;
uint16_t power_avg = 0;
uint16_t power_cap = 0;
// uint16_t power_cap_max = 0;
uint8_t temp_edge = 0;
uint8_t temp_hotspot = 0;
uint8_t temp_mem = 0;
uint8_t temp_vrm_gfx = 0;
uint8_t temp_vrm_soc = 0;
uint8_t temp_vrm_mem = 0;
bool hasGpuMetrics = false;
uint8_t usage_core = 0;
uint8_t usage_media = 0;
uint8_t usage_umc = 0;
std::string hwmon;
};
int detect(gpuSensors* target, const std::string& pci);
void update(gpuSensors* g);
} // namespace gpu
// class ThrottleStatus(IntFlag):
// # linux/drivers/gpu/drm/amd/pm/inc/amdgpu_smu.h
// PPT0 = 1 << 0
// PPT1 = 1 << 1
// PPT2 = 1 << 2
// PPT3 = 1 << 3
// SPL = 1 << 4
// FPPT = 1 << 5
// SPPT = 1 << 6
// SPPT_APU = 1 << 7
// TDC_GFX = 1 << 16
// TDC_SOC = 1 << 17
// TDC_MEM = 1 << 18
// TDC_VDD = 1 << 19
// TDC_CVIP = 1 << 20
// EDC_CPU = 1 << 21
// EDC_GFX = 1 << 22
// APCC = 1 << 23
// TEMP_GPU = 1 << 32
// TEMP_CORE = 1 << 33
// TEMP_MEM = 1 << 34
// TEMP_EDGE = 1 << 35
// TEMP_HOTSPOT = 1 << 36
// TEMP_SOC = 1 << 37
// TEMP_VR_GFX = 1 << 38
// TEMP_VR_SOC = 1 << 39
// TEMP_VR_MEM0 = 1 << 40
// TEMP_VR_MEM1 = 1 << 41
// TEMP_LIQUID0 = 1 << 42
// TEMP_LIQUID1 = 1 << 43
// VRHOT0 = 1 << 44
// VRHOT1 = 1 << 45
// PROCHOT_CPU = 1 << 46
// PROCHOT_GFX = 1 << 47
// PPM = 1 << 56
// FIT = 1 << 57
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
#ifndef HASS_HPP
#define HASS_HPP
#ifdef ENABLE_HOME_ASSISTANT
#include <libwebsockets.h>
#include <cstdint>
#include <string>
class HomeAssistant {
public:
struct Config {
std::string address;
std::string entity;
std::string token;
};
explicit HomeAssistant(const Config& config);
~HomeAssistant();
float state = 0.0f;
int connect();
void disconnect() const;
void service(int timeout_ms = 0) const;
private:
uint16_t id = 2;
std::string tx;
lws_context* context = nullptr;
lws* websock = nullptr;
void event_handler(lws* wsi, lws_callback_reasons reason, char* in);
static int callback(lws* wsi, lws_callback_reasons reason, void* user, void* in, size_t len);
Config m_config;
};
#endif
#endif

35
include/lcd.h Normal file
View File

@@ -0,0 +1,35 @@
#ifndef LCDDISPLAY_HPP
#define LCDDISPLAY_HPP
#define LCD_LINES 4
#define LCD_COLS 40
#include <stdint.h>
struct lcd_t {
int fd;
uint32_t baud;
char writebuf[LCD_LINES * LCD_COLS + 8];
char port[128];
};
typedef uint8_t lcd_glyph_t[8];
void lcd_close(struct lcd_t* lcd);
void lcd_clear(struct lcd_t* lcd);
void lcd_set_contrast(struct lcd_t* lcd, uint8_t contrast);
void lcd_set_backlight(struct lcd_t* lcd, bool enabled);
void lcd_set_cursor(struct lcd_t* lcd, uint8_t row, uint8_t col);
void lcd_set_custom_char(struct lcd_t* lcd, uint8_t index, const lcd_glyph_t* glyph);
void lcd_write_str(struct lcd_t* lcd, const char* str, uint8_t len);
void lcd_write_str_pos(struct lcd_t* lcd, const char* str, uint8_t len, uint8_t posX, uint8_t posY);
void lcd_write_buf(struct lcd_t* lcd, uint8_t row, uint8_t col, const char* str);
void lcd_clear_buf(struct lcd_t* lcd);
void lcd_flush_buf(struct lcd_t* lcd);
char lcd_bar(struct lcd_t* lcd, uint8_t char_id, double value);
int lcd_init(struct lcd_t* lcd);
inline char cc(const uint8_t c) { return (char)(c % 8); }
#endif

41
include/log.h Normal file
View File

@@ -0,0 +1,41 @@
#ifndef LOG_H
#define LOG_H
/* ── kill switch ────────────────────────────────────────────────────────── */
/* comment out to disable all logging */
#define APPLOG_ENABLED
/* comment out to disable file logging */
/* #define LOG_FILE_LOCATION "linuxSmartie.log.gz" */
/* ─────────────────────────────────────────────────────────────────────── */
#ifdef APPLOG_ENABLED
#include <stdarg.h>
typedef enum {
LOG_INF,
LOG_WRN,
LOG_ERR,
LOG_DBG,
LOG_START,
} LogLevel;
/* __PRETTY_FUNCTION__ is a GCC/Clang extension that gives the full
s ignature: "voi*d foo(int x)" — same thing source_location gave you.
Falls back to plain __func__ (C99) if somehow unavailable. */
#define applog(level, fmt, ...) \
log_write(level, __PRETTY_FUNCTION__, fmt, ##__VA_ARGS__)
/* format attribute gives compile-time printf format checking —
better than wha*t std::format gave you at runtime */
void log_write(LogLevel level, const char *func, const char *fmt, ...)
__attribute__((format(printf, 3, 4)));
#else /* APPLOG_ENABLED not defined */
#define applog(...) ((void)0)
#endif /* APPLOG_ENABLED */
#endif /* LOG_H */

View File

@@ -1,96 +0,0 @@
#ifndef LOG_HPP
#define LOG_HPP
// comment out to disable logging in general
#define applog(level, fmt, ...) utils::log(level, std::source_location::current(), fmt, ##__VA_ARGS__)
// comment out to disable logging to file
// #define LOG_FILE_LOCATION "linuxSmartie.log.gz"
#ifdef applog
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>
#include <source_location>
#include <string_view>
#ifdef LOG_FILE_LOCATION
#include <zlib.h>
inline std::mutex fmtx; // file logging mutex
#endif
enum class LOG { INF, WRN, ERR, DBG, START };
namespace utils {
inline std::mutex cmtx; // console logging mutex
constexpr std::string pFname(std::string_view full) {
if (const auto pos = full.find(' '); pos != std::string_view::npos)
full.remove_prefix(pos + 1);
if (const auto pos = full.rfind('('); pos != std::string_view::npos)
full.remove_suffix(full.size() - pos);
return std::string(full);
}
template <typename... Args>
void log(const LOG level, const std::source_location& loc, std::format_string<Args...> str, Args&&... args) {
constexpr std::string_view reset = "\033[0m";
constexpr std::string_view pattern = "[{}{}{}] ";
std::string_view color;
switch (level) {
case LOG::INF:
color = "\033[1;32m";
break; // bold; green
case LOG::WRN:
color = "\033[1;33m"; // bold; yellow
break;
case LOG::ERR:
color = "\033[1;31m"; // bold; red
break;
case LOG::DBG:
break;
case LOG::START:
color = "\033[1;3;36m"; // bold; blinking; cyan
break;
}
/* log to console */
{
std::scoped_lock lock(cmtx);
std::cout << std::format(pattern, color, pFname(loc.function_name()), reset);
std::cout << std::format(str, std::forward<Args>(args)...) << std::endl;
}
#ifdef LOG_FILE_LOCATION
/* log to file */
{
auto now = std::chrono::system_clock::now();
auto local = std::chrono::current_zone()->to_local(now);
std::string msg = std::format("{:%x %H:%M:%S }", floor<std::chrono::milliseconds>(local)); // timestamp
std::format_to(std::back_inserter(msg), pattern, color, pFname(loc.function_name()), reset); // tag
std::format_to(std::back_inserter(msg), str, std::forward<Args>(args)...); // actual log message
std::scoped_lock lock(fmtx);
gzFile file = gzopen(LOG_FILE_LOCATION, "ab");
if (file) {
gzprintf(file, "%s\n", msg.c_str());
gzclose(file);
}
}
#endif
}
} // namespace utils
#else
#define applog(...)
#endif
#endif

View File

@@ -1,49 +0,0 @@
#ifndef SYS_HPP
#define SYS_HPP
#include <cstdint>
#include <string>
namespace sys {
constexpr char months[][4] = {"sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paz", "lis", "gru"};
constexpr char wdays[][4] = {"Nie", "Pon", "Wto", "Sro", "Czw", "Pia", "Sob"};
struct sysSensors {
std::string hwmon;
uint16_t fan_speed[2] = {0}; // left and right cpu fans
int16_t t_sensor = 0; // ntc temp sensor
std::byte _pad[2];
};
struct Memory {
uint64_t free = 0;
uint64_t used = 0;
uint64_t total = 0;
};
struct Disk {
std::string mountpoint;
Memory usage;
};
struct TimeDate {
std::string time;
std::string date;
};
struct Network {
uint32_t download = 0;
uint32_t upload = 0;
};
void getTimeDate(TimeDate* td);
void getDisk(Disk* disk);
void getRam(Memory* mem);
void getNetwork(Network* net, const std::string& device);
int detect(sysSensors* s, const std::string& platform);
void updateSensors(sysSensors* s);
} // namespace sys
#endif

View File

@@ -1,80 +0,0 @@
#ifndef UTILS_HPP
#define UTILS_HPP
#include <fcntl.h>
#include <unistd.h>
#include <array>
#include <charconv>
#include <cstdint>
#include <filesystem>
#include <log.hpp>
#include <string>
namespace utils {
constexpr char fill_char = ' ';
enum just : uint8_t { right, left };
std::string pad(std::string s, uint8_t len, just j = right);
std::string center(std::string str, uint8_t len);
std::string getFirstDir(const std::string_view& path);
inline std::string pNum(const auto num, const uint8_t decs = 0) {
static std::array<char, 20> buf;
auto [ptr, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), static_cast<double>(num), std::chars_format::fixed, decs);
return {buf.data(), ptr};
}
/* converts large number in (k)bytes to human-readable numer with suffix and sane decimal */
std::string pBytes(const auto bytes, const bool isKbytes = false) {
double bytes_d = bytes;
for (constexpr std::array suffix_list = {'K', 'M', 'G', 'T', 'P', 'E', 'Z'}; const auto& suffix : suffix_list) {
if (suffix == 'K' && isKbytes) continue; // skip first loop if already in Kbytes
bytes_d /= 1024.0;
if (bytes_d < 1024) {
if (bytes_d > 100) return pNum(bytes_d) + suffix;
if (bytes_d > 10) return pNum(bytes_d, 1) + suffix;
return pNum(bytes_d, 2) + suffix;
}
}
return "++++";
}
/* reads file into a std::array buffer */
template <size_t N>
inline ssize_t readFile(const std::string& path, std::array<char, N>& buffer) {
const int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0) {
applog(LOG::ERR, "Error reading {}", path);
return 0;
}
const ssize_t len = read(fd, buffer.data(), buffer.size());
if (len >= 0) buffer[len] = '\0';
close(fd);
return len;
}
/* reads a number fast and divides it if needed */
template <uint32_t Divider = 1, typename T, size_t N>
inline void fastReadNum(const std::string& path, std::array<char, N>& buf, T& result) {
readFile(path, buf);
if constexpr (Divider == 1) {
std::from_chars(buf.data(), buf.data() + N, result);
return;
}
uint64_t temp{};
std::from_chars(buf.data(), buf.data() + N, temp);
if constexpr (std::is_floating_point_v<T>)
result = temp / static_cast<T>(Divider);
else
result = static_cast<T>(temp / Divider);
}
inline bool fileExists(const std::string_view& path) {
return std::filesystem::exists(path);
}
} // namespace utils
#endif

View File

@@ -1,20 +0,0 @@
#include <app.hpp>
int main() {
Application::Config config;
config.lcd.port = "/dev/serial/by-id/usb-Raspberry_Pi_Pico_E6616407E37AA12F-if00"; // tis a symlink, can also be just /dev/ttyACM0)
config.lcd.refresh_interval_ms = 500;
config.lcd.contrast = 215;
config.cpu_pciaddr = "0000:00:18.3";
config.gpu_pciaddr = "0000:0b:00.0";
config.sys_sensor = "nct6775.656";
config.sys_ifname = "enp4s0f0";
config.home_dir = "/home/drath";
config.hass.url = "home.drath.win";
config.hass.entity = "sensor.gniazdko_komputer_moc";
config.hass.token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0MzUxMTZiY2VjNTg0MTI3OGI4YmU0NTY4ZGQ5MzIyYiIsImlhdCI6MTc1NzQzMDI0MCwiZXhwIjoyMDcyNzkwMjQwfQ.6wKTlxEA0FLWfv-qW35vewUSvTxdVrZWqFwHT5Khu6E";
Application App(config);
return App.Run();
}

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