mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-08 18:32:02 +00:00
Compare commits
No commits in common. "c2b1aedabadb054ab28cefa81ed1ddcfbc965426" and "b510af2d8dba5383668f69cc36eba8e090fadb65" have entirely different histories.
c2b1aedaba
...
b510af2d8d
7 changed files with 84 additions and 336 deletions
56
README.md
56
README.md
|
|
@ -26,17 +26,6 @@ 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, TimeTurner 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,42 +37,19 @@ When running as a background daemon, TimeTurner provides a web interface for mon
|
|||
|
||||
---
|
||||
|
||||
## 🚀 Installation
|
||||
## 🚀 Installation (to update)
|
||||
|
||||
The `setup.sh` script is provided to compile and install the TimeTurner application and its systemd service on a Debian-based system like Raspberry Pi OS.
|
||||
For Rust install you can do
|
||||
```bash
|
||||
cargo install --git https://github.com/cjfranko/NTP-Timeturner
|
||||
```
|
||||
Clone and run the installer:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Rust and Cargo**: The script requires the Rust programming language toolchain. If you don't have it, install it from [rustup.rs](https://rustup.rs/).
|
||||
|
||||
### Running the Installer
|
||||
|
||||
1. First, clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/cjfranko/NTP-Timeturner.git
|
||||
cd NTP-Timeturner
|
||||
```
|
||||
2. Make the script executable and run it. The script will use `sudo` for commands that require root privileges, so it may ask for your password.
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
The installation script automates the following steps:
|
||||
|
||||
1. **Compiles the Binary**: Runs `cargo build --release` to create an optimised executable.
|
||||
2. **Creates Directories**: Creates `/opt/timeturner` to store the application files.
|
||||
3. **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.
|
||||
4. **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
|
||||
wget https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/setup.sh
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
---
|
||||
## 🕰️ Chrony NTP
|
||||
|
|
|
|||
8
setup.sh
8
setup.sh
|
|
@ -21,13 +21,11 @@ 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
|
||||
if [[ "$(uname)" == "Linux" ]]; then
|
||||
|
|
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -62,71 +62,71 @@
|
|||
<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 John Rogers | Have Blue Broadcast Media |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue