mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-09 02:42:02 +00:00
Merge pull request #1 from cjfranko/timeturner-and-curses
merging ltc probe and first iteration of timeturner
This commit is contained in:
commit
fdaad719b0
7 changed files with 275 additions and 221 deletions
10
config.json
Normal file
10
config.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"ltc_device": "/dev/audio",
|
||||||
|
"offset": {
|
||||||
|
"hours": 0,
|
||||||
|
"minutes": 0,
|
||||||
|
"seconds": 0,
|
||||||
|
"milliseconds": 0
|
||||||
|
},
|
||||||
|
"frame_rate": 25
|
||||||
|
}
|
||||||
109
hermione.py
109
hermione.py
|
|
@ -1,109 +0,0 @@
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
CONFIG_FILE = "hermione_config.json"
|
|
||||||
|
|
||||||
# Load config or create default
|
|
||||||
def load_config():
|
|
||||||
if not os.path.exists(CONFIG_FILE):
|
|
||||||
default_config = {
|
|
||||||
"framerate": 25,
|
|
||||||
"start_mode": "system",
|
|
||||||
"manual_time": "12:00:00",
|
|
||||||
"duration_seconds": 3600,
|
|
||||||
"ltc_gen_path": "ltc-gen.exe",
|
|
||||||
"autostart_timeout": 5
|
|
||||||
}
|
|
||||||
with open(CONFIG_FILE, "w") as f:
|
|
||||||
json.dump(default_config, f, indent=4)
|
|
||||||
return default_config
|
|
||||||
else:
|
|
||||||
with open(CONFIG_FILE, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
# Save updated config
|
|
||||||
def save_config(config):
|
|
||||||
with open(CONFIG_FILE, "w") as f:
|
|
||||||
json.dump(config, f, indent=4)
|
|
||||||
|
|
||||||
# Prompt with timeout
|
|
||||||
def prompt_with_timeout(prompt, timeout):
|
|
||||||
print(prompt, end='', flush=True)
|
|
||||||
input_data = []
|
|
||||||
|
|
||||||
def get_input():
|
|
||||||
try:
|
|
||||||
input_data.append(input())
|
|
||||||
except EOFError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
thread = threading.Thread(target=get_input)
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
thread.join(timeout)
|
|
||||||
return input_data[0] if input_data else ""
|
|
||||||
|
|
||||||
# Get timecode based on config
|
|
||||||
def get_start_time(config):
|
|
||||||
if config["start_mode"] == "system":
|
|
||||||
now = datetime.now()
|
|
||||||
return now.strftime("%H:%M:%S")
|
|
||||||
else:
|
|
||||||
return config["manual_time"]
|
|
||||||
|
|
||||||
# Run ltc-gen
|
|
||||||
def run_ltc_gen(config):
|
|
||||||
start_time = get_start_time(config)
|
|
||||||
framerate = str(config["framerate"])
|
|
||||||
duration = str(config["duration_seconds"])
|
|
||||||
ltc_gen_path = config["ltc_gen_path"]
|
|
||||||
|
|
||||||
cmd = [
|
|
||||||
ltc_gen_path,
|
|
||||||
"-f", framerate,
|
|
||||||
"-l", duration,
|
|
||||||
"-t", start_time
|
|
||||||
]
|
|
||||||
|
|
||||||
print(f"\n🎬 Running Hermione with:")
|
|
||||||
print(f" Start Time: {start_time}")
|
|
||||||
print(f" Framerate: {framerate} fps")
|
|
||||||
print(f" Duration: {duration} seconds")
|
|
||||||
print(f" Executable: {ltc_gen_path}\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.run(cmd)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"❌ Error: {ltc_gen_path} not found!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to run Hermione: {e}")
|
|
||||||
|
|
||||||
# Main logic
|
|
||||||
def main():
|
|
||||||
config = load_config()
|
|
||||||
user_input = prompt_with_timeout(
|
|
||||||
"\nPress [Enter] to run with config or type 'm' to modify (auto-starts in 5s): ",
|
|
||||||
config.get("autostart_timeout", 5)
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_input.lower() == 'm':
|
|
||||||
try:
|
|
||||||
config["framerate"] = int(input("Enter framerate (e.g. 25): "))
|
|
||||||
config["start_mode"] = input("Start from system time or manual? (system/manual): ").strip().lower()
|
|
||||||
if config["start_mode"] == "manual":
|
|
||||||
config["manual_time"] = input("Enter manual start time (HH:MM:SS): ")
|
|
||||||
config["duration_seconds"] = int(input("Enter duration in seconds: "))
|
|
||||||
config["ltc_gen_path"] = input("Enter path to ltc-gen.exe (or leave blank for default): ") or config["ltc_gen_path"]
|
|
||||||
save_config(config)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Error updating config: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
run_ltc_gen(config)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"framerate": 25,
|
|
||||||
"start_mode": "system",
|
|
||||||
"manual_time": "12:00:00",
|
|
||||||
"duration_seconds": 3600,
|
|
||||||
"ltc_gen_path": "ltc-gen.exe",
|
|
||||||
"autostart_timeout": 5
|
|
||||||
}
|
|
||||||
91
ltc_probe.py
Normal file
91
ltc_probe.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
ltc_probe.py
|
||||||
|
Advanced LTC-like signal probe using pulse duration clustering
|
||||||
|
for reliable short/long classification — works even with imbalanced timecodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import sounddevice as sd
|
||||||
|
|
||||||
|
DURATION = 1.0 # seconds
|
||||||
|
SAMPLERATE = 48000
|
||||||
|
CHANNELS = 1
|
||||||
|
MIN_EDGES = 1000
|
||||||
|
|
||||||
|
def detect_rising_edges(signal):
|
||||||
|
above_zero = signal > 0
|
||||||
|
edges = np.where(np.logical_and(~above_zero[:-1], above_zero[1:]))[0]
|
||||||
|
return edges
|
||||||
|
|
||||||
|
def cluster_durations(durations):
|
||||||
|
if len(durations) < 2:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# Use 2-means clustering (basic method)
|
||||||
|
durations = np.array(durations)
|
||||||
|
mean1, mean2 = np.min(durations), np.max(durations)
|
||||||
|
|
||||||
|
for _ in range(10): # converge in a few iterations
|
||||||
|
group1 = durations[np.abs(durations - mean1) < np.abs(durations - mean2)]
|
||||||
|
group2 = durations[np.abs(durations - mean1) >= np.abs(durations - mean2)]
|
||||||
|
if len(group1) == 0 or len(group2) == 0:
|
||||||
|
break
|
||||||
|
mean1 = np.mean(group1)
|
||||||
|
mean2 = np.mean(group2)
|
||||||
|
|
||||||
|
short = group1 if mean1 < mean2 else group2
|
||||||
|
long = group2 if mean1 < mean2 else group1
|
||||||
|
return short, long
|
||||||
|
|
||||||
|
def analyze_pulse_durations(edges, samplerate):
|
||||||
|
durations = np.diff(edges) / samplerate
|
||||||
|
if len(durations) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
short, long = cluster_durations(durations)
|
||||||
|
if short is None or long is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total = len(durations)
|
||||||
|
return {
|
||||||
|
"count": total,
|
||||||
|
"avg_width_ms": np.mean(durations) * 1000,
|
||||||
|
"short_pulses": len(short),
|
||||||
|
"long_pulses": len(long),
|
||||||
|
"short_pct": (len(short) / total) * 100,
|
||||||
|
"long_pct": (len(long) / total) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
def verdict(pulse_data):
|
||||||
|
if pulse_data is None or pulse_data["count"] < MIN_EDGES:
|
||||||
|
return "❌ No signal or not enough pulses"
|
||||||
|
elif 10 <= pulse_data["short_pct"] <= 90:
|
||||||
|
return f"✅ LTC-like bi-phase signal detected ({pulse_data['count']} pulses)"
|
||||||
|
else:
|
||||||
|
return f"⚠️ Pulse imbalance suggests non-LTC or noisy signal"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🔍 Capturing 1 second of audio for LTC probing...")
|
||||||
|
audio = sd.rec(int(DURATION * SAMPLERATE), samplerate=SAMPLERATE, channels=CHANNELS, dtype='float32')
|
||||||
|
sd.wait()
|
||||||
|
|
||||||
|
signal = audio.flatten()
|
||||||
|
edges = detect_rising_edges(signal)
|
||||||
|
pulse_data = analyze_pulse_durations(edges, SAMPLERATE)
|
||||||
|
|
||||||
|
print(f"\n📊 Pulse Analysis:")
|
||||||
|
if pulse_data:
|
||||||
|
print(f" Total pulses: {pulse_data['count']}")
|
||||||
|
print(f" Avg pulse width: {pulse_data['avg_width_ms']:.2f} ms")
|
||||||
|
print(f" Short pulses: {pulse_data['short_pulses']} ({pulse_data['short_pct']:.1f}%)")
|
||||||
|
print(f" Long pulses: {pulse_data['long_pulses']} ({pulse_data['long_pct']:.1f}%)")
|
||||||
|
else:
|
||||||
|
print(" Not enough data to analyze.")
|
||||||
|
|
||||||
|
print("\n🧭 Verdict:")
|
||||||
|
print(" ", verdict(pulse_data))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
134
setup.sh
134
setup.sh
|
|
@ -1,117 +1,43 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
|
||||||
|
|
||||||
echo ""
|
# NTP Timeturner Setup Script
|
||||||
echo "─────────────────────────────────────────────"
|
# Tested on Debian Bookworm - Raspberry Pi 3
|
||||||
echo " Welcome to the NTP Timeturner Installer"
|
# Author: cjfranko
|
||||||
echo "─────────────────────────────────────────────"
|
|
||||||
echo ""
|
|
||||||
echo "\"It's a very complicated piece of magic...\" – Hermione Granger"
|
|
||||||
echo "Initialising temporal calibration sequence..."
|
|
||||||
echo "Requesting clearance from the Ministry of Time Standards..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
echo "Step 1: Updating system packages..."
|
||||||
# Step 1: Update and upgrade packages
|
sudo apt-get update && sudo apt-get upgrade -y
|
||||||
# ---------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "Step 1: Updating package lists..."
|
|
||||||
sudo apt update
|
|
||||||
|
|
||||||
echo "Upgrading installed packages..."
|
echo "Step 2: Installing required system packages..."
|
||||||
sudo apt upgrade -y
|
sudo apt-get install -y git cmake build-essential libjack-jackd2-dev \
|
||||||
|
libsndfile1-dev libtool autoconf automake \
|
||||||
|
pkg-config libasound2-dev libfftw3-dev \
|
||||||
|
python3-full python3-venv python3-pip \
|
||||||
|
libltc-dev python3-numpy python3-matplotlib python3-sounddevice
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
echo "Step 3: Cloning libltc and ltc-tools..."
|
||||||
# Step 2: Install essential development tools
|
cd /home/hermione
|
||||||
# ---------------------------------------------------------
|
git clone https://github.com/x42/libltc.git
|
||||||
echo ""
|
git clone https://github.com/x42/ltc-tools.git
|
||||||
echo "Step 2: Installing development tools..."
|
|
||||||
sudo apt install -y git curl python3 python3-pip build-essential autoconf automake libtool cmake
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
echo "Step 4: Building libltc (the heart of our time-magic)..."
|
||||||
# Step 3: Install audio and media dependencies
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "Step 3: Installing audio libraries and tools..."
|
|
||||||
sudo apt install -y alsa-utils ffmpeg \
|
|
||||||
portaudio19-dev python3-pyaudio \
|
|
||||||
libasound2-dev libjack-jackd2-dev \
|
|
||||||
libsndfile-dev \
|
|
||||||
|| echo "Warning: Some audio dependencies may have failed to install — continuing anyway."
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Step 4: Install Python packages
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "Step 4: Installing Python packages..."
|
|
||||||
pip3 install numpy --break-system-packages
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Step 5: Build and install libltc (needed by ltc-tools)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "Step 5: Building libltc (the heart of our time-magic)..."
|
|
||||||
cd ~
|
|
||||||
if [ ! -d "libltc" ]; then
|
|
||||||
echo "Cloning libltc from GitHub..."
|
|
||||||
git clone https://github.com/x42/libltc.git
|
|
||||||
fi
|
|
||||||
cd libltc
|
cd libltc
|
||||||
|
mkdir -p build && cd build
|
||||||
echo "Preparing libltc build..."
|
cmake ..
|
||||||
./autogen.sh
|
make -j$(nproc)
|
||||||
./configure
|
|
||||||
|
|
||||||
echo "Compiling libltc..."
|
|
||||||
make
|
|
||||||
|
|
||||||
echo "Installing libltc..."
|
|
||||||
sudo make install
|
sudo make install
|
||||||
sudo ldconfig
|
sudo ldconfig
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
echo "Step 5: Building ltc-tools..."
|
||||||
# Step 6: Build and install ltc-tools
|
cd /home/hermione/ltc-tools
|
||||||
# ---------------------------------------------------------
|
make -j$(nproc)
|
||||||
echo ""
|
|
||||||
echo "Step 6: Building ltc-tools (with a gentle nudge)..."
|
|
||||||
cd ~
|
|
||||||
if [ ! -d "ltc-tools" ]; then
|
|
||||||
echo "Cloning ltc-tools from GitHub..."
|
|
||||||
git clone https://github.com/x42/ltc-tools.git
|
|
||||||
fi
|
|
||||||
cd ltc-tools
|
|
||||||
|
|
||||||
echo "Compiling ltc-tools (bypassing package check)..."
|
|
||||||
make HAVE_LIBLTC=true
|
|
||||||
|
|
||||||
echo "Installing ltc-tools..."
|
|
||||||
sudo make install
|
sudo make install
|
||||||
sudo ldconfig
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
echo "Step 6: Setting splash screen..."
|
||||||
# Step 7: Apply Custom Splash Screen
|
sudo cp /home/hermione/splash.png /usr/share/plymouth/themes/pix/splash.png
|
||||||
# ---------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "Step 7: Applying splash screen..."
|
|
||||||
|
|
||||||
sudo curl -L -o /usr/share/plymouth/themes/pix/splash.png https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/splash.png
|
echo "Step 7: Making timeturner scripts executable..."
|
||||||
sudo chmod 644 /usr/share/plymouth/themes/pix/splash.png
|
chmod +x /home/hermione/*.py
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
echo "Step 8: Setup complete. System will reboot in 30 seconds unless you press Enter..."
|
||||||
# Final Message & Reboot Option
|
echo "Press Ctrl+C or Enter now to cancel automatic reboot."
|
||||||
# ---------------------------------------------------------
|
read -t 30 -p ">> " input && sudo reboot || sudo reboot
|
||||||
echo ""
|
|
||||||
echo "─────────────────────────────────────────────"
|
|
||||||
echo " Setup Complete"
|
|
||||||
echo "─────────────────────────────────────────────"
|
|
||||||
echo ""
|
|
||||||
echo "The TimeTurner is ready. But remember:"
|
|
||||||
echo "\"You must not be seen.\" – Hermione Granger"
|
|
||||||
echo "Visual enhancements are in place. Terminal timeline is stable."
|
|
||||||
echo ""
|
|
||||||
echo "The system will reboot in 30 seconds to complete setup..."
|
|
||||||
echo "Press [Enter] to reboot immediately, or Ctrl+C to cancel."
|
|
||||||
|
|
||||||
read -t 30 -p "" || true
|
|
||||||
sleep 1
|
|
||||||
sudo reboot
|
|
||||||
|
|
|
||||||
36
test_audioinput.py
Normal file
36
test_audioinput.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
test_audioinput.py
|
||||||
|
Records 2 seconds of audio from the default input device
|
||||||
|
and saves the waveform as 'waveform.png' — works headless.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg') # Headless backend
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import sounddevice as sd
|
||||||
|
|
||||||
|
DURATION = 2 # seconds
|
||||||
|
SAMPLERATE = 48000
|
||||||
|
CHANNELS = 1
|
||||||
|
|
||||||
|
print("🔊 Recording 2 seconds from default input device...")
|
||||||
|
recording = sd.rec(int(DURATION * SAMPLERATE), samplerate=SAMPLERATE, channels=CHANNELS, dtype='float32')
|
||||||
|
sd.wait()
|
||||||
|
|
||||||
|
# Generate time axis
|
||||||
|
time_axis = np.linspace(0, DURATION, len(recording))
|
||||||
|
|
||||||
|
# Plot and save
|
||||||
|
plt.figure(figsize=(10, 4))
|
||||||
|
plt.plot(time_axis, recording, linewidth=0.5)
|
||||||
|
plt.title("Audio Input Waveform")
|
||||||
|
plt.xlabel("Time [s]")
|
||||||
|
plt.ylabel("Amplitude")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig("waveform.png")
|
||||||
|
|
||||||
|
print("✅ Waveform saved as 'waveform.png'")
|
||||||
108
timeturner.py
Normal file
108
timeturner.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
timeturner.py
|
||||||
|
NTP Timeturner Core UI
|
||||||
|
Displays LTC signal probe info using curses, updated in real-time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import curses
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
import sounddevice as sd
|
||||||
|
|
||||||
|
# --- CONFIGURATION ---
|
||||||
|
SAMPLERATE = 48000
|
||||||
|
CHANNELS = 1
|
||||||
|
PROBE_INTERVAL = 1.0 # seconds
|
||||||
|
MIN_EDGES = 1000
|
||||||
|
|
||||||
|
status = {
|
||||||
|
"count": 0,
|
||||||
|
"avg_width_ms": 0.0,
|
||||||
|
"short_pct": 0.0,
|
||||||
|
"long_pct": 0.0,
|
||||||
|
"verdict": "Waiting for signal...",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- LTC PROBE THREAD ---
|
||||||
|
def detect_rising_edges(signal):
|
||||||
|
above_zero = signal > 0
|
||||||
|
edges = np.where(np.logical_and(~above_zero[:-1], above_zero[1:]))[0]
|
||||||
|
return edges
|
||||||
|
|
||||||
|
def cluster_durations(durations):
|
||||||
|
if len(durations) < 2:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
durations = np.array(durations)
|
||||||
|
mean1, mean2 = np.min(durations), np.max(durations)
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
group1 = durations[np.abs(durations - mean1) < np.abs(durations - mean2)]
|
||||||
|
group2 = durations[np.abs(durations - mean1) >= np.abs(durations - mean2)]
|
||||||
|
if len(group1) == 0 or len(group2) == 0:
|
||||||
|
break
|
||||||
|
mean1 = np.mean(group1)
|
||||||
|
mean2 = np.mean(group2)
|
||||||
|
|
||||||
|
short = group1 if mean1 < mean2 else group2
|
||||||
|
long = group2 if mean1 < mean2 else group1
|
||||||
|
return short, long
|
||||||
|
|
||||||
|
def analyze_signal():
|
||||||
|
global status
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
audio = sd.rec(int(PROBE_INTERVAL * SAMPLERATE), samplerate=SAMPLERATE,
|
||||||
|
channels=CHANNELS, dtype='float32')
|
||||||
|
sd.wait()
|
||||||
|
signal = audio.flatten()
|
||||||
|
edges = detect_rising_edges(signal)
|
||||||
|
durations = np.diff(edges) / SAMPLERATE
|
||||||
|
short, long = cluster_durations(durations)
|
||||||
|
|
||||||
|
if short is None or long is None or len(durations) < MIN_EDGES:
|
||||||
|
status["verdict"] = "❌ No signal or not enough pulses"
|
||||||
|
continue
|
||||||
|
|
||||||
|
status.update({
|
||||||
|
"count": len(durations),
|
||||||
|
"avg_width_ms": np.mean(durations) * 1000,
|
||||||
|
"short_pct": (len(short) / len(durations)) * 100,
|
||||||
|
"long_pct": (len(long) / len(durations)) * 100,
|
||||||
|
"verdict": "✅ LTC-like signal detected" if 10 <= (len(short) / len(durations)) * 100 <= 90
|
||||||
|
else "⚠️ Pulse imbalance — possible noise or non-LTC"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
status["verdict"] = f"⚠️ Audio error: {e}"
|
||||||
|
|
||||||
|
# --- CURSES UI ---
|
||||||
|
def draw_ui(stdscr):
|
||||||
|
curses.curs_set(0)
|
||||||
|
stdscr.nodelay(True)
|
||||||
|
stdscr.timeout(500)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
stdscr.clear()
|
||||||
|
stdscr.addstr(0, 2, "🕰️ NTP Timeturner - Live LTC Monitor", curses.A_BOLD)
|
||||||
|
stdscr.addstr(2, 4, f"Pulses captured: {status['count']}")
|
||||||
|
stdscr.addstr(3, 4, f"Avg pulse width: {status['avg_width_ms']:.2f} ms")
|
||||||
|
stdscr.addstr(4, 4, f"Short pulse ratio: {status['short_pct']:.1f}%")
|
||||||
|
stdscr.addstr(5, 4, f"Long pulse ratio: {status['long_pct']:.1f}%")
|
||||||
|
stdscr.addstr(7, 4, f"Status: {status['verdict']}")
|
||||||
|
stdscr.addstr(9, 4, "Press Ctrl+C to exit.")
|
||||||
|
stdscr.refresh()
|
||||||
|
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
|
||||||
|
# --- ENTRY POINT ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
probe_thread = threading.Thread(target=analyze_signal, daemon=True)
|
||||||
|
probe_thread.start()
|
||||||
|
curses.wrapper(draw_ui)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue