A real-time hardware temperature monitor built with Qt 6 + C++ + lm-sensors, packaged as .deb for both amd64 and arm64.
This repository ships two binaries from the same source tree:
qtmonitor— the full GUI application (Qt Widgets + OpenGL).mon-cli— a headless command-line companion built with Qt Core + Qt Network only. No X / Wayland needed, so it runs on servers, Jetsons, Raspberry Pi, or any GPU-less box.
Both binaries are produced by a single build_packages.sh run and are version-locked together.
mon-cli # interactive menu (1 → Monitor, 2 → help; Ctrl+C exits)
mon-cli monitor # open the Monitor live table directly
mon-cli sensors [--json] # one-shot dump of detected temperatures (with timestamp)
mon-cli collect [opts] # foreground: HTTP ingest server (receive pushed readings)
mon-cli sync [opts] # foreground: push local readings to a remote URL
mon-cli logger [opts] # background: record sensor data to CSV via systemd
mon-cli status # show collect / sync / logger service state + logger live info
mon-cli logs [collect|sync|logger] [-f] [-n N] # journalctl for services
mon-cli service install # write systemd unit files for collect/sync/logger
mon-cli service uninstall # stop, disable and remove the unit files
mon-cli (then pick 1) or mon-cli monitor opens a full-screen ANSI table that
refreshes at 100 ms. It shows:
- Live per-sensor temperatures
- Logger status block when a
mon-cli loggeris running:- state (RUNNING / STOPPED)
- elapsed time
- recording start time
- target stop time (when
-twas used) - CSV output path
- per-sensor min / avg / max accumulated so far
Ctrl+C exits cleanly.
Both support -t/--time for auto-exit:
# Run a 30-minute collection server on port 9000
mon-cli collect --port 9000 -t 30m
# Push this host's readings for 2 hours, then exit
mon-cli sync --url http://peer:8000/ingest -t 2h
# Mixed-case and combined durations are accepted
mon-cli sync --url http://peer:8000/ingest -t 1H30MDuration format: 4h / 5M / 6s (case-insensitive, combinable). Maximum 48 h.
mon-cli can install three systemd system services into
/etc/systemd/system/:
| Unit | Role |
|---|---|
mon-cli-collect.service |
HTTP ingest server (remote collection) |
mon-cli-sync.service |
Push local readings to a peer |
mon-cli-logger.service |
Record readings to CSV in the background |
# One-time: install unit files (does not start anything)
sudo mon-cli service install
# Start / stop individual services. `start` rewrites the unit's ExecStart
# with the given arguments, then enables + restarts the service.
sudo mon-cli collect start --port 9000
sudo mon-cli sync start --url http://peer:9000/ingest --interval 1000
sudo mon-cli logger start --output-dir /var/log/mon-cli --stats -t 4h
sudo mon-cli collect stop
sudo mon-cli sync stop
sudo mon-cli logger stop
# Inspect
mon-cli status # all three services + logger live stats
mon-cli collect status
mon-cli sync status
mon-cli logger status # systemctl + live stats from the status file
mon-cli logs # journalctl for all three
mon-cli logs logger -f # follow logger output
mon-cli logs sync -n 500 # last 500 lines
# Remove everything
sudo mon-cli service uninstallmon-cli logger samples all detected sensors (or a --sensors subset) into
a bounded in-memory cache and writes a CSV to the output directory when it
stops — either because -t expired, mon-cli logger stop was issued, or the
service was stopped by systemd.
# Record all sensors for 4 hours, then stop and flush to CSV
sudo mon-cli logger start -t 4h
# Record forever (until stopped), include avg/min/max columns
sudo mon-cli logger start --stats --output-dir /var/log/mon-cli
# Record only two sensors at 500 ms intervals with a 16 MB cache
sudo mon-cli logger start \
--sensors k10temp-pci-00c3/Tctl,nvme-pci-0100/Composite \
--interval 500 --cache-mb 16Options:
| Option | Default | Meaning |
|---|---|---|
--output-dir DIR |
/var/log/mon-cli |
CSV destination directory |
--interval MS |
100 |
Sample interval (milliseconds) |
--cache-mb MB |
8 |
In-memory ring-buffer budget |
--stats |
off | Append (avg) (min) (max) columns |
--sensors a,b |
all detected | Restrict to a subset |
-t, --time DUR |
none | Auto-stop after 4h / 5M / 6s (max 48h) |
CSV format matches the GUI's export:
timestamp(s),sensor1(°C),sensor2(°C),...
0.105,50.63,44.50,...
0.210,50.50,49.00,...
With --stats, each sensor additionally contributes three columns:
sensor1(avg), sensor1(min), sensor1(max).
The logger publishes its live state to /run/mon-cli/logger.status (root /
systemd) or /tmp/mon-cli-<uid>/logger.status (foreground non-root). mon-cli monitor and mon-cli status read this file to show the logger's state,
elapsed time, start time, target stop time, and per-sensor min/avg/max.
- OpenGL-accelerated chart on supported hardware (
QOpenGLWidget); automatically falls back to Qt's CPU raster engine when no GPU / GL context is available - Adaptive aggregation by data span, not just by window setting:
- When the actual recorded span for a sensor is < 5 minutes, the chart always uses the raw 50 ms samples with per-pixel min/max decimation — the LOD aggregation does not kick in prematurely, so short recordings look crisp even if the user picks a 5-minute window
- Once the span reaches 5 minutes (or older data has spilled to disk), the chart switches to the LOD path described below
- Trend + peak preserving LOD aggregation for long windows:
- The model maintains a per-second level-of-detail
(avg, min, max)per sensor - Each output pixel column emits up to four points —
(enterT, avg) → (minT, minV) → (maxT, maxV) → (exitT, avg)— so spikes and troughs stay visible and adjacent columns connect with a smooth trend line (no sawtooth) - Flat columns collapse to two points, eliminating noise on steady signals
- The model maintains a per-second level-of-detail
- Stable right edge: the still-open second bucket is popped out of the closed-bucket aggregation and re-appended every tick as a "partial column" (enter anchor / min / max / exit at the current wall-clock time). The historical curve is served from cache while the live edge tracks 50 ms samples, so the rightmost pixel no longer vibrates
- Per-sensor downsampled-data cache keyed by window + pixel width + the integer second of the latest sample, so the closed-bucket aggregation is recomputed at most once per second instead of once per 50 ms tick
- Cached visible-window bounds avoid re-scanning every point on every frame
- Y-axis auto-scale uses the raw min/max of the visible window plus the live open bucket's extremes, so sudden spikes correctly push the axis range up even when the displayed line is aggregated
- Refreshes on the same timer as the chart but reuses
QTableWidgetItems instead of allocating new ones every tick - Column widths are only recomputed when the sensor list changes, not on every refresh
trimToTimeWindowis throttled to ~1 Hz (was: every 50 ms tick), removing a recurring O(N) scan of every sensor's timestamp vector
- CSV (timestamp + all sensors)
- Chart snapshot as PNG / JPG / BMP
- Combined report (CSV + image) via Export dialog
- Filenames include
yyyyMMdd_HHmmsstimestamp
- POST JSON (hostname, timestamp, interval_ms, sensor data) to a user-configurable URL
- Per-sensor checkbox to choose which curves to send
- Configurable send interval (ms)
- Start / Stop buttons in the control bar; status shown in bottom-right status bar
- CLI demo receiver (
scripts/cli_demo_remote_sync.py) prints received data
sudo apt update
sudo apt install -y build-essential qt6-base-dev qt6-base-dev:arm64 \
qt6-base-dev-tools \
libsensors-dev libsensors-dev:arm64 \
debhelper fakeroot dpkg-cross \
gcc-aarch64-linux-gnuIf you have not configured sensors yet:
sudo sensors-detect # answer YES to scan, save
sensors # verify sensors are detectedcd ~/Desktop/qtmonitor
mkdir -p build
cd build
qmake6 ../qtmonitor.pro # GUI
make -j$(nproc)
./qtmonitor
# CLI companion
mkdir -p ../build-cli && cd ../build-cli
qmake6 ../mon-cli.pro
make -j$(nproc)
./mon-cliBoth builds are out-of-tree: build/ and build-cli/ hold all generated
objects, so the project root stays clean.
Run the provided script from the project root:
cd ~/Desktop/qtmonitor
./build_packages.shThis will:
- Create
output/if it does not exist - Clean previously built
.deb/.ddeb/.changes/.buildinfoartifacts - Build the amd64 packages (
qtmonitorandmon-cli) withdpkg-buildpackage - Cross-compile both binaries for arm64 and stage matching
.debs - Move all resulting packages into
output/ - Remove transient
build/,build-cli/,build-arm64/,build-cli-arm64/directories
Outputs in output/:
qtmonitor_0.0.3-1_amd64.deb/qtmonitor_0.0.3-1_arm64.debmon-cli_0.0.3-1_amd64.deb/mon-cli_0.0.3-1_arm64.debqtmonitor_0.0.3-1_amd64.changes/.buildinfo*-dbgsym_0.0.3-1_amd64.ddeb(when produced)
Python scripts (.py) anywhere in the project are never deleted by the cleanup step.
# amd64 (both qtmonitor and mon-cli debs)
dpkg-buildpackage -us -uc -b
# arm64 cross-compile (GUI)
mkdir -p build-arm64
( cd build-arm64 && \
aarch64-linux-gnu-qmake6 \
-spec linux-aarch64-gnu-g++ CONFIG+=release ../qtmonitor.pro && \
make -j$(nproc) )
# arm64 cross-compile (CLI)
mkdir -p build-cli-arm64
( cd build-cli-arm64 && \
aarch64-linux-gnu-qmake6 \
-spec linux-aarch64-gnu-g++ CONFIG+=release ../mon-cli.pro && \
make -j$(nproc) )# GUI
sudo dpkg -i output/qtmonitor_0.0.3-1_amd64.deb
# CLI (optional — can be installed independently on headless hosts)
sudo dpkg -i output/mon-cli_0.0.3-1_amd64.deb
sudo apt -f install # resolve any missing depsThe mon-cli package depends only on libqt6core6*, libqt6network6,
libsensors5 — no Qt Gui / Widgets / OpenGL — so it's safe to install on
servers without a display stack.
qtmonitor
# or: /usr/bin/qtmonitorThe first launch shows a warning if lm-sensors is not initialized; the program falls back to a UI-only mode. Run sensors-detect once to enable real readings.
mon-cli # interactive menu
mon-cli monitor # live table, 100 ms refresh
mon-cli sensors # one-shot dump
mon-cli sensors --json # machine-readable output# Terminal 1: receiver
python3 -u scripts/cli_demo_remote_sync.py
# Listening on http://0.0.0.0:8000/ingest
# Terminal 2 (GUI): Tools → Remote Sync → URL http://127.0.0.1:8000/ingest
# OR Terminal 2 (CLI):
mon-cli sync --url http://127.0.0.1:8000/ingestThe CLI receiver prints formatted JSON:
[2026-07-21T11:40:00] hostname=workstation interval_ms=1000
coretemp-isa-0000/Core 0 49.50 C
coretemp-isa-0000/Core 1 51.25 C
You can also chain two mon-cli instances:
# Host A
mon-cli collect --port 8000
# Host B
mon-cli sync --url http://host-a:8000/ingesttranslationmanager.cpp is a single-source dictionary covering both languages. The QT_TR(key) macro resolves the current language's text at runtime. (GUI only — mon-cli is English-only.)
- The program runs completely offline; remote sync is opt-in to a user-configured URL.
- All CPU work stays in the GUI thread; the chart and table refresh on a configurable timer (40 ms ~ 5 s).
- Memory is bounded by the data window:
temperaturemodel.cpptrims samples older than the configured retention window roughly once per second, and spills older history to temporary files on disk when the in-memory budget is exceeded. - The
arm64.debis cross-compiled; install it on Jetson / Raspberry Pi / Apple Silicon (Asahi) systems. - On systems without hardware OpenGL, Qt automatically uses a software raster path through
QOpenGLWidget's internal fallback, so the same GUI binary works on headless or GPU-less machines.mon-clihas no GL dependency at all. mon-cli loggerwrites its live status to/run/mon-cli/logger.status(when running as a system service) or/tmp/mon-cli-<uid>/logger.status(foreground, non-root).mon-cli monitorandmon-cli statuspick up whichever exists.