diff --git a/README.md b/README.md index 8a7b357..5f94e52 100644 --- a/README.md +++ b/README.md @@ -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://: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 stratumβ€―10 +# Serve the system clock as a reference at stratumβ€―1 server 127.127.1.0 allow 127.0.0.0/8 -local stratum 10 +local stratum 1 Add to bottom: # Allow LAN clients diff --git a/config.yml b/config.yml index bf892f4..bc552ee 100644 --- a/config.yml +++ b/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: 1 - minutes: 2 - seconds: 3 - frames: 4 - milliseconds: 5 + hours: 0 + minutes: 0 + seconds: 0 + frames: 0 + milliseconds: 0 diff --git a/setup.sh b/setup.sh index 4dd5685..0b68c12 100644 --- a/setup.sh +++ b/setup.sh @@ -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 < "$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 < - NTP TimeTurner + Fetch | Hachi @@ -62,74 +62,74 @@

Controls

-
- - + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+ +
+ + + + +
-
- - -
-
- -
-
- - +
+ + +
+
+ Logs Icon +

Logs

-
- - -
-
- - -
-
- - -
-
- - +
+

                         
-
- - - -
-
- - - - - -
-
- - - - -
-
-
- - -
-
- Logs Icon -

Logs

-
-
-

-                
-
-
diff --git a/static/index_dev.html b/static/index_dev.html new file mode 100644 index 0000000..edc555d --- /dev/null +++ b/static/index_dev.html @@ -0,0 +1,141 @@ + + + + + + NTP TimeTurner + + + + +
+ + + + + +
+ +
+

LTC Input

+

--:--:--:--

+
+ + + +
+
+ + +
+

NTP Clock

+

--:--:--.---

+

---- -- --

+
+ + + + +
+

Ξ” -- ms (-- frames)

+
+ + +
+
+ Network Icon +

Network

+
+

--

+
+ + +
+
+ Controls Icon +

Controls

+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + + + + +
+
+ + + + +
+
+
+ + +
+
+ Logs Icon +

Logs

+
+
+

+                        
+
+
+ +
+ + + + + diff --git a/static/style.css b/static/style.css index f9bcd58..bc53cce 100644 --- a/static/style.css +++ b/static/style.css @@ -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 { diff --git a/timeturner.service b/timeturner.service new file mode 100644 index 0000000..f3daec8 --- /dev/null +++ b/timeturner.service @@ -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 diff --git a/update.sh b/update.sh new file mode 100644 index 0000000..ad9fcb9 --- /dev/null +++ b/update.sh @@ -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" \ No newline at end of file