diff --git a/README.md b/README.md index 5f94e52..8a7b357 100644 --- a/README.md +++ b/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://: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 diff --git a/config.yml b/config.yml index bc552ee..bf892f4 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: 0 - minutes: 0 - seconds: 0 - frames: 0 - milliseconds: 0 + hours: 1 + minutes: 2 + seconds: 3 + frames: 4 + milliseconds: 5 diff --git a/setup.sh b/setup.sh index 0b68c12..4dd5685 100644 --- a/setup.sh +++ b/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 < "$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 < /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:" diff --git a/src/main.rs b/src/main.rs index 8006681..ab9fa94 100644 --- a/src/main.rs +++ b/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(); diff --git a/static/index.html b/static/index.html index 02bb279..7178fb9 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@ - Fetch | Hachi + NTP TimeTurner @@ -62,74 +62,74 @@

Controls

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

Logs

-
-
-

-                        
-
+ +
+
+ Logs Icon +

Logs

+
+

+                
+
+ diff --git a/static/index_dev.html b/static/index_dev.html deleted file mode 100644 index edc555d..0000000 --- a/static/index_dev.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - 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 bc53cce..f9bcd58 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: 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 { diff --git a/timeturner.service b/timeturner.service deleted file mode 100644 index f3daec8..0000000 --- a/timeturner.service +++ /dev/null @@ -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 diff --git a/update.sh b/update.sh deleted file mode 100644 index ad9fcb9..0000000 --- a/update.sh +++ /dev/null @@ -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" \ No newline at end of file