mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-08 18:32: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
|
||||
set -e
|
||||
#!/bin/bash
|
||||
|
||||
echo ""
|
||||
echo "─────────────────────────────────────────────"
|
||||
echo " Welcome to the NTP Timeturner Installer"
|
||||
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 ""
|
||||
# NTP Timeturner Setup Script
|
||||
# Tested on Debian Bookworm - Raspberry Pi 3
|
||||
# Author: cjfranko
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Step 1: Update and upgrade packages
|
||||
# ---------------------------------------------------------
|
||||
echo ""
|
||||
echo "Step 1: Updating package lists..."
|
||||
sudo apt update
|
||||
echo "Step 1: Updating system packages..."
|
||||
sudo apt-get update && sudo apt-get upgrade -y
|
||||
|
||||
echo "Upgrading installed packages..."
|
||||
sudo apt upgrade -y
|
||||
echo "Step 2: Installing required system packages..."
|
||||
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
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Step 2: Install essential development tools
|
||||
# ---------------------------------------------------------
|
||||
echo ""
|
||||
echo "Step 2: Installing development tools..."
|
||||
sudo apt install -y git curl python3 python3-pip build-essential autoconf automake libtool cmake
|
||||
echo "Step 3: Cloning libltc and ltc-tools..."
|
||||
cd /home/hermione
|
||||
git clone https://github.com/x42/libltc.git
|
||||
git clone https://github.com/x42/ltc-tools.git
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 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
|
||||
echo "Step 4: Building libltc (the heart of our time-magic)..."
|
||||
cd libltc
|
||||
|
||||
echo "Preparing libltc build..."
|
||||
./autogen.sh
|
||||
./configure
|
||||
|
||||
echo "Compiling libltc..."
|
||||
make
|
||||
|
||||
echo "Installing libltc..."
|
||||
mkdir -p build && cd build
|
||||
cmake ..
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Step 6: Build and install ltc-tools
|
||||
# ---------------------------------------------------------
|
||||
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..."
|
||||
echo "Step 5: Building ltc-tools..."
|
||||
cd /home/hermione/ltc-tools
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Step 7: Apply Custom Splash Screen
|
||||
# ---------------------------------------------------------
|
||||
echo ""
|
||||
echo "Step 7: Applying splash screen..."
|
||||
echo "Step 6: Setting splash screen..."
|
||||
sudo cp /home/hermione/splash.png /usr/share/plymouth/themes/pix/splash.png
|
||||
|
||||
sudo curl -L -o /usr/share/plymouth/themes/pix/splash.png https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/splash.png
|
||||
sudo chmod 644 /usr/share/plymouth/themes/pix/splash.png
|
||||
echo "Step 7: Making timeturner scripts executable..."
|
||||
chmod +x /home/hermione/*.py
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Final Message & Reboot Option
|
||||
# ---------------------------------------------------------
|
||||
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
|
||||
echo "Step 8: Setup complete. System will reboot in 30 seconds unless you press Enter..."
|
||||
echo "Press Ctrl+C or Enter now to cancel automatic reboot."
|
||||
read -t 30 -p ">> " input && sudo reboot || 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