Compare commits

...

60 commits

Author SHA1 Message Date
Chris Frankland-Wright
2e8bc9ac5e updated some masthead and readme
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Has been cancelled
2025-08-31 22:13:47 +01:00
Chris Frankland-Wright
3e423416a8 fixed error in naming of service 2025-08-31 21:54:18 +01:00
Chris Frankland-Wright
4a07b29728 removed captive portal for now 2025-08-31 18:42:58 +01:00
Chris Frankland-Wright
2d46fccfbe include ifup ifdown
Some checks are pending
Build for Raspberry Pi / Build for aarch64 (push) Waiting to run
2025-08-31 11:00:55 +01:00
Chris Frankland-Wright
fdddf4eb76 revert to dchcpcd 2025-08-31 10:56:22 +01:00
Chris Frankland-Wright
46892884a1 ignore all eth for dnsq 2025-08-31 10:48:46 +01:00
Chris Frankland-Wright
04165f2686 last chance saloon!
Some checks are pending
Build for Raspberry Pi / Build for aarch64 (push) Waiting to run
2025-08-31 00:19:24 +01:00
Chris Frankland-Wright
459e44250e ugh another try... 2025-08-31 00:06:32 +01:00
Chris Frankland-Wright
604d118d25 force dns and dhcp 2025-08-30 23:59:34 +01:00
Chris Frankland-Wright
320174fe53 final attempt for the night 2025-08-30 23:54:12 +01:00
Chris Frankland-Wright
8903d6d006 DHCP issues 2025-08-30 23:48:24 +01:00
Chris Frankland-Wright
32e785bd88 update installer with reinstall options 2025-08-30 23:39:05 +01:00
Chris Frankland-Wright
fb4ecc5f2a bug fix for AP captive 2025-08-30 23:25:06 +01:00
Chris Frankland-Wright
0c51fd77fa rename AP to Hachi 2025-08-30 23:15:47 +01:00
Chris Frankland-Wright
474e62d487 created updater 2025-08-30 23:02:48 +01:00
Chris Frankland-Wright
ea55d087b5 change default yaml to not have timeturning
Some checks are pending
Build for Raspberry Pi / Build for aarch64 (push) Waiting to run
2025-08-30 22:52:42 +01:00
Chris Frankland-Wright
af6dbcc9a7 added in chrony settings 2025-08-30 22:42:32 +01:00
Chris Frankland-Wright
169c9b9aef allow update of nodogsplash 2025-08-30 22:32:03 +01:00
Chris Frankland-Wright
6221eea98c removed pip for install 2025-08-30 22:29:25 +01:00
Chris Frankland-Wright
ac035a8e0b fix tmp and install python 2025-08-30 22:27:28 +01:00
Chris Frankland-Wright
f2e2fa9c7f renambled dns thing 2025-08-30 22:22:02 +01:00
Chris Frankland-Wright
3c73a0487b fix nodog install issue 2025-08-30 22:19:30 +01:00
Chris Frankland-Wright
360e0751f2 ugh... 2025-08-30 22:16:04 +01:00
Chris Frankland-Wright
a764b4d4ad remove dnsmasq 2025-08-30 22:12:32 +01:00
Chris Frankland-Wright
63bd17b71e asdfghjk 2025-08-30 22:07:17 +01:00
Chris Frankland-Wright
7db595259f more network config 2025-08-30 22:05:09 +01:00
Chris Frankland-Wright
e19b50fe2b moved nodogsplash to nmtui 2025-08-30 22:01:11 +01:00
Chris Frankland-Wright
cc1335f1a9 blah 2025-08-30 21:56:44 +01:00
Chris Frankland-Wright
5ca32b6f36 premature exit issue 2025-08-30 21:54:17 +01:00
Chris Frankland-Wright
1caa09ac46 added delay in process 2025-08-30 21:51:03 +01:00
Chris Frankland-Wright
57de9a98a5 updated to network-manager 2025-08-30 21:46:38 +01:00
Chris Frankland-Wright
0e7b583829 fix bug with service creation 2025-08-30 21:43:00 +01:00
Chris Frankland-Wright
e4c59b412b install json handler 2025-08-30 21:40:15 +01:00
Chris Frankland-Wright
dad5c2d06a update to pull nodogsplash and configure 2025-08-30 21:37:25 +01:00
Chris Frankland-Wright
baf674edd8 updated version of dogsplash 2025-08-30 21:20:26 +01:00
Chris Frankland-Wright
762f872e7c updated to pull directly 2025-08-30 21:17:39 +01:00
Chris Frankland-Wright
5eb706601f updated for nodogsplash 2025-08-30 21:14:09 +01:00
Chris Frankland-Wright
7773e62402 Merge branch 'main' of https://github.com/cjfranko/NTP-Timeturner 2025-08-30 21:08:26 +01:00
Chris Frankland-Wright
24c09fa233 updated setup.sh file 2025-08-30 21:07:48 +01:00
Chris Frankland-Wright
7c5b7fe031
Revise README for clarity and accuracy
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Failing after 13s
Updated project description and corrected a typo.
2025-08-29 13:12:10 +01:00
Chris Frankland-Wright
01c0d0495f
Rename project to 'Fetch | Hachi' and revise text
Updated project name and refined description.
2025-08-29 13:11:20 +01:00
Chris Frankland-Wright
3f99488ea0 include yaml 2025-08-26 12:09:53 +01:00
Chris Frankland-Wright
e2d48391ea create localised hotspot
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Failing after 27s
2025-08-26 11:19:53 +01:00
Chris Frankland-Wright
8362435e12 feat: Implement custom Plymouth splash screen installation
Co-authored-by: aider (gemini/gemini-2.5-flash) <aider@aider.chat>
2025-08-24 13:16:19 +01:00
Chris Frankland-Wright
cd9ac5a141 feat: Add system package update to setup script
Co-authored-by: aider (gemini/gemini-2.5-flash) <aider@aider.chat>
2025-08-24 13:07:08 +01:00
Chris Frankland-Wright
b6a7606e1a feat: Add automated dependency installation for Rust, Chrony, NMTUI, and adjtimex
Co-authored-by: aider (gemini/gemini-2.5-flash) <aider@aider.chat>
2025-08-24 13:06:07 +01:00
Chris Frankland-Wright
9c57c32c68
Update README.md
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Failing after 44s
2025-08-22 22:36:41 +01:00
Chris Frankland-Wright
c2b1aedaba
Merge pull request #32 from cjfranko/updated-web-ui-setup
Some checks failed
Build for Raspberry Pi / Build for aarch64 (push) Failing after 20s
Updated web UI setup
2025-08-12 16:48:40 +01:00
Chris Frankland-Wright
a009dd35c9 updated web ui 2025-08-12 16:28:32 +01:00
Chris Frankland-Wright
4d0b4ebae4 docs: Detail setup.sh installation process in README
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:27:44 +01:00
Chris Frankland-Wright
5d206b564b style: Set button font to Arial
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:26:03 +01:00
Chris Frankland-Wright
b03d935a9e style: Reduce background image size 2025-08-12 16:25:58 +01:00
Chris Frankland-Wright
cbacf14ca1 Style: Improve Timeturner offset layout with compact inputs and side labels
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:19:53 +01:00
Chris Frankland-Wright
22ac073922 style: Update button and input field styling
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:16:39 +01:00
Chris Frankland-Wright
acab0fbc04 style: Hide hardware offset, auto sync, and nudge controls
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:13:43 +01:00
Chris Frankland-Wright
048ae41739 feat: Restore hardware offset, auto sync, and nudge controls 2025-08-12 16:13:35 +01:00
Chris Frankland-Wright
1075be6e24 hide sections 2025-08-12 16:06:38 +01:00
Chris Frankland-Wright
8e369a2e3a fix: Ensure static web assets are installed and clarify service config
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:02:25 +01:00
Chris Frankland-Wright
af0a512187 docs: Document web interface and clarify API server startup
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 16:00:43 +01:00
Chris Frankland-Wright
95fcb6f26a feat: Add systemd service for TimeTurner auto-start
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-08-12 15:58:21 +01:00
9 changed files with 636 additions and 101 deletions

100
README.md
View file

@ -1,16 +1,16 @@
# 🕰️ NTP Timeturner (alpha)
# Fetch | Hachi (alpha)
**An LTC-driven NTP server for Raspberry Pi, built with broadcast precision and a hint of magic.**
**An LTC-driven NTP server for Raspberry Pi, built with broadcast precision**
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.
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.
Created by Chris Frankland-Wright and John Rogers
Created by Chris Frankland-Wright and Chaos Rogers
---
## 📦 Hardware Requirements
- Raspberry Pi 5 (Dev Platform) but should be supported by Pi v3 (or better)
- Raspberry Pi 5 2GB (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,6 +26,17 @@ Created by Chris Frankland-Wright and John 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.
---
@ -37,20 +48,79 @@ Created by Chris Frankland-Wright and John Rogers
---
## 🚀 Installation (to update)
## 🚀 Installation
For Rust install you can do
```bash
cargo install --git https://github.com/cjfranko/NTP-Timeturner
```
Clone and run the installer:
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:
```bash
wget https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/setup.sh
chmod +x setup.sh
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 && \
./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
@ -59,10 +129,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 stratum10
# Serve the system clock as a reference at stratum1
server 127.127.1.0
allow 127.0.0.0/8
local stratum 10
local stratum 1
Add to bottom:
# Allow LAN clients

View file

@ -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: 1
minutes: 2
seconds: 3
frames: 4
milliseconds: 5
hours: 0
minutes: 0
seconds: 0
frames: 0
milliseconds: 0

241
setup.sh
View file

@ -3,14 +3,226 @@ set -e
echo "--- TimeTurner Setup ---"
# 1. Build the release binary
echo "📦 Building release binary with Cargo..."
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."
# 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
cargo build --release
echo "✅ Build complete."
@ -21,13 +233,16 @@ echo "🔧 Creating directories..."
sudo mkdir -p $INSTALL_DIR
echo "✅ Directory $INSTALL_DIR created."
# 3. Install binary
echo "🚀 Installing timeturner binary..."
# 3. Install binary and static web files
echo "🚀 Installing timeturner binary and web assets..."
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 installed to $INSTALL_DIR and linked to $BIN_DIR."
echo "✅ Binary and assets installed to $INSTALL_DIR, and binary 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/
@ -42,7 +257,13 @@ echo ""
echo "--- Setup Complete ---"
echo "The TimeTurner daemon is now installed."
echo "The working directory is $INSTALL_DIR."
echo "A default 'config.yml' will be created there on first run."
# 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 ""
if [[ "$(uname)" == "Linux" ]]; then
echo "To start the service, run:"

View file

@ -193,8 +193,10 @@ async fn main() {
});
}
// 5⃣ Spawn UI or setup daemon logging
// 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.
if args.command.is_none() {
// --- Interactive TUI Mode ---
log::info!("🔧 Watching config.yml...");
log::info!("🚀 Serial thread launched");
log::info!("🖥️ UI thread launched");
@ -205,8 +207,10 @@ 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 systemd service will capture it. The web service (API and static files)
// is launched later in the main async block.
log::info!("🚀 Starting TimeTurner daemon...");
}
@ -282,7 +286,10 @@ async fn main() {
let local = LocalSet::new();
local
.run_until(async move {
// 8⃣ Spawn the API server thread
// 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.
{
let api_state = ltc_state.clone();
let config_clone = config.clone();

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NTP TimeTurner</title>
<title>Fetch | Hachi</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
@ -62,45 +62,45 @@
<h2>Controls</h2>
</div>
<div class="collapsible-content" id="controls-content">
<div class="control-group">
<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">
<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 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 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 style="display: flex; flex-direction: column;">
<label for="offset-m">Minutes</label>
<input type="number" id="offset-m" style="width: 60px;">
<div class="offset-control">
<input type="number" id="offset-m" min="-99" max="99">
<label for="offset-m">min</label>
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-s">Seconds</label>
<input type="number" id="offset-s" style="width: 60px;">
<div class="offset-control">
<input type="number" id="offset-s" min="-99" max="99">
<label for="offset-s">sec</label>
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-f">Frames</label>
<input type="number" id="offset-f" style="width: 60px;">
<div class="offset-control">
<input type="number" id="offset-f" min="-99" max="99">
<label for="offset-f">fr</label>
</div>
<div style="display: flex; flex-direction: column;">
<label for="offset-ms">Milliseconds</label>
<input type="number" id="offset-ms" style="width: 60px;">
<div class="offset-control">
<input type="number" id="offset-ms">
<label for="offset-ms">ms</label>
</div>
</div>
</div>
<div class="control-group">
<button id="save-config">Save Config</button>
<button id="manual-sync">Manual Sync</button>
<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">
<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;">
@ -129,7 +129,7 @@
</div>
<footer>
<p>
Built by Chris Frankland-Wright and John Rogers | Have Blue Broadcast Media |
Built by Chris Frankland-Wright and Chaos 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>

141
static/index_dev.html Normal file
View file

@ -0,0 +1,141 @@
<!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>

View file

@ -15,7 +15,7 @@ body {
background-repeat: no-repeat;
background-position: bottom 20px right 20px;
background-attachment: fixed;
background-size: 300px;
background-size: 100px;
color: #333;
margin: 0;
padding: 20px;
@ -88,25 +88,75 @@ body {
gap: 10px;
}
input[type="number"] {
input[type="number"],
input[type="text"] {
padding: 8px;
border: 1px solid #ccc;
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"] {
width: 80px;
}
input[type="text"] {
width: auto;
}
button {
padding: 8px 15px;
border: none;
border-radius: 4px;
background-color: #007bff;
background-color: #1a7db6;
color: white;
cursor: pointer;
font-size: 14px;
font-family: Arial, sans-serif;
font-weight: bold;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
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;
}
#sync-message {

18
timeturner.service Normal file
View file

@ -0,0 +1,18 @@
[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 Normal file
View file

@ -0,0 +1,28 @@
#!/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"