mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-08 18:32:02 +00:00
Compare commits
No commits in common. "main" and "v0.1.0_RC" have entirely different histories.
9 changed files with 102 additions and 637 deletions
100
README.md
100
README.md
|
|
@ -1,16 +1,16 @@
|
|||
# Fetch | Hachi (alpha)
|
||||
# 🕰️ NTP Timeturner (alpha)
|
||||
|
||||
**An LTC-driven NTP server for Raspberry Pi, built with broadcast precision**
|
||||
**An LTC-driven NTP server for Raspberry Pi, built with broadcast precision and a hint of magic.**
|
||||
|
||||
Hachi synchronises timecode-locked systems by decoding incoming LTC (Linear Time Code) and broadcasting it as NTP/PTP — with the dedication our namesake would insist upon.
|
||||
Inspired by the TimeTurner in the Harry Potter series, this project synchronises timecode-locked systems by decoding incoming LTC (Linear Time Code) and broadcasting it as NTP — with precision as Hermione would insist upon.
|
||||
|
||||
Created by Chris Frankland-Wright and Chaos Rogers
|
||||
Created by Chris Frankland-Wright and John Rogers
|
||||
|
||||
---
|
||||
|
||||
## 📦 Hardware Requirements
|
||||
|
||||
- Raspberry Pi 5 2GB (Dev Platform) but should be supported by Pi v3 (or better)
|
||||
- Raspberry Pi 5 (Dev Platform) but should be supported by Pi v3 (or better)
|
||||
- Debian Bookworm (64-bit recommended)
|
||||
- Teensy 4.0 - https://thepihut.com/products/teensy-4-0-headers
|
||||
- Audio Adapter Board for Teensy 4.0 (Rev D) - https://thepihut.com/products/audio-adapter-board-for-teensy-4-0
|
||||
|
|
@ -26,17 +26,6 @@ Created by Chris Frankland-Wright and Chaos Rogers
|
|||
- Broadcasts time via local NTP server
|
||||
- Supports configurable time offsets (hours, minutes, seconds, frames or milliseconds)
|
||||
- Systemd service support for headless operation
|
||||
- Web-based UI for monitoring and control when running as a daemon
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Web Interface & API
|
||||
|
||||
When running as a background daemon, Hachi provides a web interface for monitoring and configuration.
|
||||
|
||||
- **Access**: The web UI is available at `http://<raspberry_pi_ip>:8080`.
|
||||
- **Functionality**: You can view the real-time sync status, see logs, and change all configuration options directly from your browser.
|
||||
- **API**: A JSON API is also exposed for programmatic access. See `docs/api.md` for full details.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -48,79 +37,20 @@ When running as a background daemon, Hachi provides a web interface for monitori
|
|||
|
||||
---
|
||||
|
||||
## 🚀 Installation
|
||||
## 🚀 Installation (to update)
|
||||
|
||||
The `setup.sh` script compiles and installs the Hachi application. You can run it by cloning the repository with `git` or by using the `curl` command below for a git-free installation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Internet Connection**: To download dependencies.
|
||||
- **Curl and Unzip**: The script requires `curl` to download files and `unzip` for the git-free method. The setup script will attempt to install these if they are missing.
|
||||
|
||||
### Running the Installer (Recommended)
|
||||
|
||||
This command downloads the latest version, unpacks it, and runs the setup script. Paste it into your Raspberry Pi terminal:
|
||||
For Rust install you can do
|
||||
```bash
|
||||
cargo install --git https://github.com/cjfranko/NTP-Timeturner
|
||||
```
|
||||
Clone and run the installer:
|
||||
|
||||
```bash
|
||||
curl -L https://github.com/cjfranko/NTP-Timeturner/archive/refs/heads/main.zip -o NTP-Timeturner.zip && \
|
||||
unzip NTP-Timeturner.zip && \
|
||||
cd NTP-Timeturner-main && \
|
||||
chmod +x setup.sh && \
|
||||
wget https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/setup.sh
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
The installation script automates the following steps:
|
||||
|
||||
1. **Installs Dependencies**: Installs `git`, `curl`, `unzip`, and necessary build tools.
|
||||
2. **Compiles the Binary**: Runs `cargo build --release` to create an optimised executable.
|
||||
3. **Creates Directories**: Creates `/opt/timeturner` to store the application files.
|
||||
4. **Installs Files**:
|
||||
- The compiled binary is copied to `/opt/timeturner/timeturner`.
|
||||
- The web interface assets from the `static/` directory are copied to `/opt/timeturner/static`.
|
||||
- A symbolic link is created from `/usr/local/bin/timeturner` to the binary, allowing it to be run from any location.
|
||||
5. **Sets up Systemd Service**:
|
||||
- Copies the `timeturner.service` file to `/etc/systemd/system/`.
|
||||
- Enables the service to start automatically on system boot.
|
||||
|
||||
After installation is complete, the script will provide instructions to start the service manually or to run the application in its interactive terminal mode.
|
||||
|
||||
```bash
|
||||
The working directory is /opt/timeturner.
|
||||
Default 'config.yml' installed to /opt/timeturner.
|
||||
|
||||
To start the service, run:
|
||||
sudo systemctl start timeturner.service
|
||||
|
||||
To view live logs, run:
|
||||
journalctl -u timeturner.service -f
|
||||
|
||||
To run the interactive TUI instead, simply run from the project directory:
|
||||
cargo run
|
||||
Or from anywhere after installation:
|
||||
timeturner
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Updating
|
||||
|
||||
If you installed Hachi by cloning the repository with `git`, you can use the `update.sh` script to easily update to the latest version.
|
||||
|
||||
**Note**: This script will not work if you used the `curl` one-line command for installation, as that method does not create a Git repository.
|
||||
|
||||
To run the update script, navigate to the `NTP-Timeturner-main` directory and run:
|
||||
```bash
|
||||
chmod +x update.sh && ./update.sh
|
||||
```
|
||||
|
||||
The update script automates the following:
|
||||
1. Pulls the latest code from the `main` branch on GitHub.
|
||||
2. Rebuilds the application binary.
|
||||
3. Copies the new binary to `/opt/timeturner/`.
|
||||
4. Restarts the `timeturner` service to apply the changes.
|
||||
|
||||
---
|
||||
## 🕰️ Chrony NTP
|
||||
```bash
|
||||
|
|
@ -129,10 +59,10 @@ chronyc tracking | NTP Tracking
|
|||
sudo nano /etc/chrony/chrony.conf | Default Chrony Conf File
|
||||
|
||||
Add to top:
|
||||
# Serve the system clock as a reference at stratum 1
|
||||
# Serve the system clock as a reference at stratum 10
|
||||
server 127.127.1.0
|
||||
allow 127.0.0.0/8
|
||||
local stratum 1
|
||||
local stratum 10
|
||||
|
||||
Add to bottom:
|
||||
# Allow LAN clients
|
||||
|
|
|
|||
10
config.yml
10
config.yml
|
|
@ -12,8 +12,8 @@ defaultNudgeMs: 2
|
|||
# Time-turning offsets. All values are added to the incoming LTC time.
|
||||
# These can be positive or negative.
|
||||
timeturnerOffset:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
frames: 0
|
||||
milliseconds: 0
|
||||
hours: 1
|
||||
minutes: 2
|
||||
seconds: 3
|
||||
frames: 4
|
||||
milliseconds: 5
|
||||
|
|
|
|||
241
setup.sh
241
setup.sh
|
|
@ -3,226 +3,14 @@ set -e
|
|||
|
||||
echo "--- TimeTurner Setup ---"
|
||||
|
||||
# Check if TimeTurner is already installed.
|
||||
INSTALL_DIR="/opt/timeturner"
|
||||
if [ -f "${INSTALL_DIR}/timeturner" ]; then
|
||||
echo "✅ TimeTurner is already installed."
|
||||
# Ask the user what to do
|
||||
read -p "Do you want to (U)pdate, (R)einstall, or (A)bort? [U/r/a] " choice
|
||||
case "$choice" in
|
||||
r|R )
|
||||
echo "Proceeding with full re-installation..."
|
||||
# Stop the service to allow overwriting the binary, ignore errors if not running
|
||||
echo "Stopping existing TimeTurner service..."
|
||||
sudo systemctl stop timeturner.service || true
|
||||
# The script will continue to the installation steps below.
|
||||
;;
|
||||
a|A )
|
||||
echo "Aborting setup."
|
||||
exit 0
|
||||
;;
|
||||
* ) # Default to Update
|
||||
echo "Attempting to run the update script..."
|
||||
# Ensure we are in a git repository and the update script exists
|
||||
if [ -d ".git" ] && [ -f "update.sh" ]; then
|
||||
chmod +x update.sh
|
||||
./update.sh
|
||||
# Exit cleanly after the update
|
||||
exit 0
|
||||
else
|
||||
echo "⚠️ Could not find 'update.sh' or not in a git repository."
|
||||
echo "Please re-clone the repository to get the update script, or remove the existing installation to run setup again:"
|
||||
echo " sudo rm -rf ${INSTALL_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
|
||||
# Determine package manager
|
||||
PKG_MANAGER=""
|
||||
if command -v apt &> /dev/null; then
|
||||
PKG_MANAGER="apt"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
PKG_MANAGER="dnf"
|
||||
elif command -v pacman &> /dev/null; then
|
||||
PKG_MANAGER="pacman"
|
||||
else
|
||||
echo "Error: No supported package manager (apt, dnf, pacman) found. Please install dependencies manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected package manager: $PKG_MANAGER"
|
||||
|
||||
# --- Update System Packages ---
|
||||
echo "Updating system packages..."
|
||||
if [ "$PKG_MANAGER" == "apt" ]; then
|
||||
sudo apt update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::="--force-confold"
|
||||
elif [ "$PKG_MANAGER" == "dnf" ]; then
|
||||
sudo dnf upgrade -y
|
||||
elif [ "$PKG_MANAGER" == "pacman" ]; then
|
||||
sudo pacman -Syu --noconfirm
|
||||
fi
|
||||
echo "System packages updated."
|
||||
|
||||
# --- Install Rust/Cargo if not installed ---
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "Rust/Cargo not found. Installing Rustup..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
# Source cargo's env for the current shell session
|
||||
# This is for the current script's execution path, typically rustup adds to .bashrc/.profile for future sessions.
|
||||
# We need it now, but for non-interactive script, sourcing won't affect parent shell.
|
||||
# However, cargo build below will rely on it being in PATH. rustup makes sure of this if it installs.
|
||||
# For safety, ensure PATH is updated.
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
echo "Rust/Cargo installed successfully."
|
||||
else
|
||||
echo "Rust/Cargo is already installed."
|
||||
fi
|
||||
|
||||
# --- Install common build dependencies for Rust ---
|
||||
echo "Installing common build dependencies..."
|
||||
if [ "$PKG_MANAGER" == "apt" ]; then
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential libudev-dev pkg-config curl wget
|
||||
elif [ "$PKG_MANAGER" == "dnf" ]; then
|
||||
sudo dnf install -y gcc make perl-devel libudev-devel pkg-config curl wget
|
||||
elif [ "$PKG_MANAGER" == "pacman" ]; then
|
||||
sudo pacman -Sy --noconfirm base-devel libudev pkg-config curl
|
||||
fi
|
||||
echo "Common build dependencies installed."
|
||||
|
||||
# --- Install Python dependencies for testing ---
|
||||
echo "🐍 Installing Python dependencies for test scripts..."
|
||||
if [ "$PKG_MANAGER" == "apt" ]; then
|
||||
# We no longer need hotspot dependencies
|
||||
sudo apt install -y python3 python3-pip python3-serial
|
||||
elif [ "$PKG_MANAGER" == "dnf" ]; then
|
||||
# python3-pyserial is the name for pyserial in dnf
|
||||
sudo dnf install -y python3 python3-pip python3-pyserial
|
||||
elif [ "$PKG_MANAGER" == "pacman" ]; then
|
||||
# python-pyserial is the name for pyserial in pacman
|
||||
sudo pacman -Sy --noconfirm python python-pip python-pyserial
|
||||
fi
|
||||
# sudo pip3 install pyserial # This is replaced by the native package manager installs above
|
||||
echo "✅ Python dependencies installed."
|
||||
|
||||
# --- Apply custom splash screen ---
|
||||
if [[ "$(uname)" == "Linux" ]]; then
|
||||
echo "🖼️ Applying custom splash screen..."
|
||||
SPLASH_URL="https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/refs/heads/main/splash.png"
|
||||
PLYMOUTH_THEME_DIR="/usr/share/plymouth/themes/pix"
|
||||
PLYMOUTH_IMAGE_PATH="${PLYMOUTH_THEME_DIR}/splash.png"
|
||||
|
||||
sudo mkdir -p "${PLYMOUTH_THEME_DIR}"
|
||||
echo "Downloading splash image from ${SPLASH_URL}..."
|
||||
sudo curl -L "${SPLASH_URL}" -o "${PLYMOUTH_IMAGE_PATH}"
|
||||
|
||||
if [ -f "${PLYMOUTH_IMAGE_PATH}" ]; then
|
||||
echo "Splash image downloaded. Updating Plymouth configuration..."
|
||||
# Set 'pix' as the default plymouth theme if not already.
|
||||
# This is a common theme that expects splash.png.
|
||||
sudo update-alternatives --install /usr/share/plymouth/themes/default.plymouth default.plymouth "${PLYMOUTH_THEME_DIR}/pix.plymouth" 100 || true
|
||||
# Ensure the pix theme exists and is linked
|
||||
if [ ! -f "${PLYMOUTH_THEME_DIR}/pix.plymouth" ]; then
|
||||
echo "Creating dummy pix.plymouth for update-initramfs"
|
||||
echo "[Plymouth Theme]" | sudo tee "${PLYMOUTH_THEME_DIR}/pix.plymouth" > /dev/null
|
||||
echo "Name=Pi Splash" | sudo tee -a "${PLYMOUTH_THEME_DIR}/pix.plymouth" > /dev/null
|
||||
echo "Description=TimeTurner Raspberry Pi Splash Screen" | sudo tee -a "${PLYMOUTH_THEME_DIR}/pix.plymouth" > /dev/null
|
||||
echo "SpriteAnimation=/splash.png" | sudo tee -a "${PLYMOUTH_THEME_DIR}/pix.plymouth" > /dev/null
|
||||
fi
|
||||
|
||||
# Update the initial RAM filesystem to include the new splash screen
|
||||
sudo update-initramfs -u
|
||||
echo "✅ Custom splash screen applied. Reboot may be required to see changes."
|
||||
else
|
||||
echo "❌ Failed to download splash image from ${SPLASH_URL}."
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Skipping splash screen configuration on non-Linux OS."
|
||||
fi
|
||||
|
||||
# --- Remove NTPD and install Chrony, NMTUI, Adjtimex ---
|
||||
echo "Removing NTPD (if installed) and installing Chrony, NMTUI, Adjtimex..."
|
||||
|
||||
# --- Remove NTPD and install Chrony, NMTUI, Adjtimex ---
|
||||
echo "Removing NTPD (if installed) and installing Chrony, NMTUI, Adjtimex..."
|
||||
|
||||
if [ "$PKG_MANAGER" == "apt" ]; then
|
||||
sudo apt update
|
||||
sudo apt remove -y ntp || true # Remove ntp if it exists, ignore if not
|
||||
sudo apt install -y chrony network-manager adjtimex
|
||||
sudo systemctl enable chrony --now
|
||||
elif [ "$PKG_MANAGER" == "dnf" ]; then
|
||||
sudo dnf remove -y ntp
|
||||
sudo dnf install -y chrony NetworkManager-tui adjtimex
|
||||
sudo systemctl enable chronyd --now
|
||||
elif [ "$PKG_MANAGER" == "pacman" ]; then
|
||||
sudo pacman -Sy --noconfirm ntp || true
|
||||
sudo pacman -R --noconfirm ntp || true # Ensure ntp is removed
|
||||
sudo pacman -Sy --noconfirm chrony networkmanager adjtimex
|
||||
sudo systemctl enable chronyd --now
|
||||
sudo systemctl enable NetworkManager --now # nmtui relies on NetworkManager
|
||||
fi
|
||||
|
||||
echo "NTPD removed (if present). Chrony, NMTUI, and Adjtimex installed and configured."
|
||||
|
||||
# --- Configure Chrony to act as a local NTP server ---
|
||||
echo "⚙️ Configuring Chrony to serve local time..."
|
||||
# The path to chrony.conf can vary
|
||||
if [ -f /etc/chrony/chrony.conf ]; then
|
||||
CHRONY_CONF="/etc/chrony/chrony.conf"
|
||||
elif [ -f /etc/chrony.conf ]; then
|
||||
CHRONY_CONF="/etc/chrony.conf"
|
||||
else
|
||||
CHRONY_CONF=""
|
||||
fi
|
||||
|
||||
if [ -n "$CHRONY_CONF" ]; then
|
||||
# Comment out any existing pool, server, or sourcedir lines to prevent syncing with external sources
|
||||
echo "Disabling external NTP sources..."
|
||||
sudo sed -i -E 's/^(pool|server|sourcedir)/#&/' "$CHRONY_CONF"
|
||||
|
||||
# Add settings to the top of the file to serve local clock
|
||||
# Using a temp file to prepend is safer than multiple sed calls
|
||||
TEMP_CONF=$(mktemp)
|
||||
cat <<EOF > "$TEMP_CONF"
|
||||
# Serve the system clock as a reference at stratum 1
|
||||
server 127.127.1.0
|
||||
allow 127.0.0.0/8
|
||||
local stratum 1
|
||||
|
||||
EOF
|
||||
# Append the rest of the original config file after our new lines
|
||||
cat "$CHRONY_CONF" >> "$TEMP_CONF"
|
||||
sudo mv "$TEMP_CONF" "$CHRONY_CONF"
|
||||
|
||||
|
||||
# Add settings to the bottom of the file to allow LAN clients
|
||||
echo "Allowing LAN clients..."
|
||||
sudo tee -a "$CHRONY_CONF" > /dev/null <<EOF
|
||||
|
||||
# Allow LAN clients to connect
|
||||
allow 0.0.0.0/0
|
||||
EOF
|
||||
|
||||
# Restart chrony to apply changes (service name can be chrony or chronyd)
|
||||
echo "Restarting Chrony service..."
|
||||
sudo systemctl restart chrony || sudo systemctl restart chronyd
|
||||
echo "✅ Chrony configured."
|
||||
else
|
||||
echo "⚠️ Warning: chrony.conf not found. Skipping Chrony configuration."
|
||||
fi
|
||||
|
||||
|
||||
# --- The entire WiFi hotspot and captive portal section has been removed ---
|
||||
|
||||
|
||||
# 1. Build the release binary
|
||||
echo "📦 Building release binary with Cargo..."
|
||||
# No need to check for cargo again, as it's handled above
|
||||
if ! command -v cargo &> /dev/null
|
||||
then
|
||||
echo "❌ Cargo is not installed. Please install Rust and Cargo first."
|
||||
echo "Visit https://rustup.rs/ for instructions."
|
||||
exit 1
|
||||
fi
|
||||
cargo build --release
|
||||
echo "✅ Build complete."
|
||||
|
||||
|
|
@ -233,16 +21,13 @@ echo "🔧 Creating directories..."
|
|||
sudo mkdir -p $INSTALL_DIR
|
||||
echo "✅ Directory $INSTALL_DIR created."
|
||||
|
||||
# 3. Install binary and static web files
|
||||
echo "🚀 Installing timeturner binary and web assets..."
|
||||
# 3. Install binary
|
||||
echo "🚀 Installing timeturner binary..."
|
||||
sudo cp target/release/ntp_timeturner $INSTALL_DIR/timeturner
|
||||
# The static directory contains the web UI files
|
||||
sudo cp -r static $INSTALL_DIR/
|
||||
sudo ln -sf $INSTALL_DIR/timeturner $BIN_DIR/timeturner
|
||||
echo "✅ Binary and assets installed to $INSTALL_DIR, and binary linked to $BIN_DIR."
|
||||
echo "✅ Binary installed to $INSTALL_DIR and linked to $BIN_DIR."
|
||||
|
||||
# 4. Install systemd service file
|
||||
# Only needed for Linux systems (e.g., Raspberry Pi OS)
|
||||
if [[ "$(uname)" == "Linux" ]]; then
|
||||
echo "⚙️ Installing systemd service for Linux..."
|
||||
sudo cp timeturner.service /etc/systemd/system/
|
||||
|
|
@ -257,13 +42,7 @@ echo ""
|
|||
echo "--- Setup Complete ---"
|
||||
echo "The TimeTurner daemon is now installed."
|
||||
echo "The working directory is $INSTALL_DIR."
|
||||
# Copy default config.yml from repo if it exists
|
||||
if [ -f config.yml ]; then
|
||||
sudo cp config.yml $INSTALL_DIR/
|
||||
echo "Default 'config.yml' installed to $INSTALL_DIR."
|
||||
else
|
||||
echo "⚠️ No default 'config.yml' found in repository. Please add one if needed."
|
||||
fi
|
||||
echo "A default 'config.yml' will be created there on first run."
|
||||
echo ""
|
||||
if [[ "$(uname)" == "Linux" ]]; then
|
||||
echo "To start the service, run:"
|
||||
|
|
|
|||
15
src/main.rs
15
src/main.rs
|
|
@ -193,13 +193,11 @@ async fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
// 5️⃣ Spawn UI or setup daemon logging. The web service is only started
|
||||
// when running as a daemon. The TUI is for interactive foreground use.
|
||||
// 5️⃣ Spawn UI or setup daemon logging
|
||||
if args.command.is_none() {
|
||||
// --- Interactive TUI Mode ---
|
||||
log::info!("🔧 Watching config.yml...");
|
||||
log::info!("🚀 Serial thread launched");
|
||||
log::info!("🖥️ UI thread launched");
|
||||
log::info!("🖥️ UI thread launched");
|
||||
let ui_state = ltc_state.clone();
|
||||
let config_clone = config.clone();
|
||||
let port = serial_port_path;
|
||||
|
|
@ -207,10 +205,8 @@ async fn main() {
|
|||
start_ui(ui_state, port, config_clone);
|
||||
});
|
||||
} else {
|
||||
// --- Daemon Mode ---
|
||||
// In daemon mode, logging is already set up to go to stderr.
|
||||
// The systemd service will capture it. The web service (API and static files)
|
||||
// is launched later in the main async block.
|
||||
// The systemd service will capture it.
|
||||
log::info!("🚀 Starting TimeTurner daemon...");
|
||||
}
|
||||
|
||||
|
|
@ -286,10 +282,7 @@ async fn main() {
|
|||
let local = LocalSet::new();
|
||||
local
|
||||
.run_until(async move {
|
||||
// 8️⃣ Spawn the API server task.
|
||||
// This server provides the JSON API and serves the static web UI files
|
||||
// from the `static/` directory. It runs in both TUI and daemon modes,
|
||||
// but is primarily for the web UI used in daemon mode.
|
||||
// 8️⃣ Spawn the API server thread
|
||||
{
|
||||
let api_state = ltc_state.clone();
|
||||
let config_clone = config.clone();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fetch | Hachi</title>
|
||||
<title>NTP TimeTurner</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
|
@ -62,74 +62,74 @@
|
|||
<h2>Controls</h2>
|
||||
</div>
|
||||
<div class="collapsible-content" id="controls-content">
|
||||
<div class="control-group" style="display: none;">
|
||||
<label for="hw-offset">Hardware Offset (ms):</label>
|
||||
<input type="number" id="hw-offset" name="hw-offset">
|
||||
</div>
|
||||
<div class="control-group" style="display: none;">
|
||||
<input type="checkbox" id="auto-sync-enabled" name="auto-sync-enabled" style="vertical-align: middle;">
|
||||
<label for="auto-sync-enabled" style="vertical-align: middle;">Enable Auto Sync</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Timeturner Offset</label>
|
||||
<div class="offset-controls-container">
|
||||
<div class="offset-control">
|
||||
<input type="number" id="offset-h" min="-99" max="99">
|
||||
<label for="offset-h">hr</label>
|
||||
</div>
|
||||
<div class="offset-control">
|
||||
<input type="number" id="offset-m" min="-99" max="99">
|
||||
<label for="offset-m">min</label>
|
||||
</div>
|
||||
<div class="offset-control">
|
||||
<input type="number" id="offset-s" min="-99" max="99">
|
||||
<label for="offset-s">sec</label>
|
||||
</div>
|
||||
<div class="offset-control">
|
||||
<input type="number" id="offset-f" min="-99" max="99">
|
||||
<label for="offset-f">fr</label>
|
||||
</div>
|
||||
<div class="offset-control">
|
||||
<input type="number" id="offset-ms">
|
||||
<label for="offset-ms">ms</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="hw-offset">Hardware Offset (ms):</label>
|
||||
<input type="number" id="hw-offset" name="hw-offset">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<input type="checkbox" id="auto-sync-enabled" name="auto-sync-enabled" style="vertical-align: middle;">
|
||||
<label for="auto-sync-enabled" style="vertical-align: middle;">Enable Auto Sync</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Timeturner Offset</label>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-h">Hours</label>
|
||||
<input type="number" id="offset-h" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-m">Minutes</label>
|
||||
<input type="number" id="offset-m" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-s">Seconds</label>
|
||||
<input type="number" id="offset-s" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-f">Frames</label>
|
||||
<input type="number" id="offset-f" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-ms">Milliseconds</label>
|
||||
<input type="number" id="offset-ms" style="width: 60px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="save-config">Save Timeturner Config</button>
|
||||
<button id="manual-sync">Send Manual Sync</button>
|
||||
<span id="sync-message"></span>
|
||||
</div>
|
||||
<div class="control-group" style="display: none;">
|
||||
<label>Nudge Clock (ms):</label>
|
||||
<button id="nudge-down">-</button>
|
||||
<input type="number" id="nudge-value" style="width: 60px;">
|
||||
<button id="nudge-up">+</button>
|
||||
<span id="nudge-message"></span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="date-input">Set System Date:</label>
|
||||
<input type="text" id="date-input" placeholder="YYYY-MM-DD" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
<button id="set-date">Set Date</button>
|
||||
<span id="date-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="save-config">Save Config</button>
|
||||
<button id="manual-sync">Manual Sync</button>
|
||||
<span id="sync-message"></span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Nudge Clock (ms):</label>
|
||||
<button id="nudge-down">-</button>
|
||||
<input type="number" id="nudge-value" style="width: 60px;">
|
||||
<button id="nudge-up">+</button>
|
||||
<span id="nudge-message"></span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="date-input">Set System Date:</label>
|
||||
<input type="text" id="date-input" placeholder="YYYY-MM-DD" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
<button id="set-date">Set Date</button>
|
||||
<span id="date-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="card full-width collapsible-card">
|
||||
<div class="toggle-header" id="logs-toggle">
|
||||
<img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
|
||||
<h2>Logs</h2>
|
||||
</div>
|
||||
<div class="collapsible-content" id="logs-content">
|
||||
<pre id="logs" class="log-box"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Logs -->
|
||||
<div class="card full-width collapsible-card">
|
||||
<div class="toggle-header" id="logs-toggle">
|
||||
<img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
|
||||
<h2>Logs</h2>
|
||||
</div>
|
||||
<div class="collapsible-content" id="logs-content">
|
||||
<pre id="logs" class="log-box"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>
|
||||
Built by Chris Frankland-Wright and Chaos Rogers | Have Blue Broadcast Media |
|
||||
Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media |
|
||||
<a href="https://github.com/cjfranko/NTP-Timeturner" target="_blank" rel="noopener noreferrer">https://github.com/cjfranko/NTP-Timeturner</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NTP TimeTurner</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="assets/header.png" alt="NTP Timeturner" class="header-logo">
|
||||
|
||||
<!-- Mock Data Controls (hidden by default) -->
|
||||
<div id="mock-controls" class="card full-width" style="display: none;">
|
||||
<h2>Mock Data Controls</h2>
|
||||
<div class="control-group">
|
||||
<label for="mock-data-selector">Select Mock Data Scenario:</label>
|
||||
<select id="mock-data-selector"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- LTC Status -->
|
||||
<div class="card">
|
||||
<h2>LTC Input</h2>
|
||||
<p id="ltc-timecode">--:--:--:--</p>
|
||||
<div class="icon-group">
|
||||
<span id="ltc-status"></span>
|
||||
<span id="frame-rate"></span>
|
||||
<span id="lock-ratio"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Clock & Sync -->
|
||||
<div class="card">
|
||||
<h2>NTP Clock</h2>
|
||||
<p id="system-clock">--:--:--.---</p>
|
||||
<p class="system-date-display"><span id="system-date">---- -- --</span></p>
|
||||
<div class="icon-group">
|
||||
<span id="ntp-active"></span>
|
||||
<span id="sync-status"></span>
|
||||
<span id="jitter-status"></span>
|
||||
<span id="delta-status"></span>
|
||||
</div>
|
||||
<p id="delta-text">Δ -- ms (-- frames)</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Interfaces -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="assets/timeturner_network.png" class="header-icon" alt="Network Icon">
|
||||
<h2>Network</h2>
|
||||
</div>
|
||||
<p id="interfaces">--</p>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="card full-width collapsible-card">
|
||||
<div class="toggle-header" id="controls-toggle">
|
||||
<img src="assets/timeturner_controls.png" class="toggle-icon" alt="Controls Icon">
|
||||
<h2>Controls</h2>
|
||||
</div>
|
||||
<div class="collapsible-content" id="controls-content">
|
||||
<div class="control-group">
|
||||
<label for="hw-offset">Hardware Offset (ms):</label>
|
||||
<input type="number" id="hw-offset" name="hw-offset">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<input type="checkbox" id="auto-sync-enabled" name="auto-sync-enabled" style="vertical-align: middle;">
|
||||
<label for="auto-sync-enabled" style="vertical-align: middle;">Enable Auto Sync</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Timeturner Offset</label>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-h">Hours</label>
|
||||
<input type="number" id="offset-h" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-m">Minutes</label>
|
||||
<input type="number" id="offset-m" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-s">Seconds</label>
|
||||
<input type="number" id="offset-s" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-f">Frames</label>
|
||||
<input type="number" id="offset-f" style="width: 60px;">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<label for="offset-ms">Milliseconds</label>
|
||||
<input type="number" id="offset-ms" style="width: 60px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="save-config">Save Config</button>
|
||||
<button id="manual-sync">Manual Sync</button>
|
||||
<span id="sync-message"></span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Nudge Clock (ms):</label>
|
||||
<button id="nudge-down">-</button>
|
||||
<input type="number" id="nudge-value" style="width: 60px;">
|
||||
<button id="nudge-up">+</button>
|
||||
<span id="nudge-message"></span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="date-input">Set System Date:</label>
|
||||
<input type="text" id="date-input" placeholder="YYYY-MM-DD" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
<button id="set-date">Set Date</button>
|
||||
<span id="date-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="card full-width collapsible-card">
|
||||
<div class="toggle-header" id="logs-toggle">
|
||||
<img src="assets/timeturner_logs.png" class="toggle-icon" alt="Logs Icon">
|
||||
<h2>Logs</h2>
|
||||
</div>
|
||||
<div class="collapsible-content" id="logs-content">
|
||||
<pre id="logs" class="log-box"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>
|
||||
Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media |
|
||||
<a href="https://github.com/cjfranko/NTP-Timeturner" target="_blank" rel="noopener noreferrer">https://github.com/cjfranko/NTP-Timeturner</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="icon-map.js"></script>
|
||||
<script src="mock-data.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -15,7 +15,7 @@ body {
|
|||
background-repeat: no-repeat;
|
||||
background-position: bottom 20px right 20px;
|
||||
background-attachment: fixed;
|
||||
background-size: 100px;
|
||||
background-size: 300px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
|
|
@ -88,75 +88,25 @@ body {
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
input[type="number"],
|
||||
input[type="text"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #9fb3c8;
|
||||
border-radius: 4px;
|
||||
background-color: #f0f4f8;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
input[type="number"]:focus,
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #1a7db6;
|
||||
box-shadow: 0 0 0 2px rgba(26, 125, 182, 0.2);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #1a7db6;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #166999;
|
||||
}
|
||||
|
||||
.offset-controls-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.offset-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.offset-control input[type="number"] {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.offset-control label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#offset-ms {
|
||||
width: 60px;
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#sync-message {
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
[Unit]
|
||||
Description=NTP TimeTurner Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
# The 'timeturner daemon' command starts the background process.
|
||||
# It requires 'config.yml' and the 'static/' web assets directory
|
||||
# to be present in the WorkingDirectory.
|
||||
ExecStart=/opt/timeturner/timeturner daemon
|
||||
WorkingDirectory=/opt/timeturner
|
||||
PIDFile=/opt/timeturner/ntp_timeturner.pid
|
||||
Restart=always
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
28
update.sh
28
update.sh
|
|
@ -1,28 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "--- TimeTurner Update Script ---"
|
||||
|
||||
# 1. Fetch the latest changes from the git repository
|
||||
echo "🔄 Pulling latest changes from GitHub..."
|
||||
git pull origin main
|
||||
|
||||
# 2. Rebuild the release binary
|
||||
echo "📦 Building release binary with Cargo..."
|
||||
cargo build --release
|
||||
|
||||
# 3. Stop the currently running service to release the file lock
|
||||
echo "🛑 Stopping TimeTurner service..."
|
||||
sudo systemctl stop timeturner.service || true
|
||||
|
||||
# 4. Copy the new binary to the installation directory
|
||||
echo "🚀 Deploying new binary..."
|
||||
sudo cp target/release/ntp_timeturner /opt/timeturner/timeturner
|
||||
|
||||
# 5. Restart the service with the new binary
|
||||
echo "✅ Restarting TimeTurner service..."
|
||||
sudo systemctl restart timeturner.service
|
||||
|
||||
echo ""
|
||||
echo "Update complete. To check the status of the service, run:"
|
||||
echo " systemctl status timeturner.service"
|
||||
Loading…
Add table
Add a link
Reference in a new issue