diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ff82f28 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" # Location of Cargo.toml + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..596ae03 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,70 @@ +name: Build for Raspberry Pi + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + CARGO_TERM_COLOR: always + # Target for 64-bit Raspberry Pi (Raspberry Pi OS) + RUST_TARGET: aarch64-unknown-linux-gnu + +jobs: + build: + name: Build for aarch64 + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ env.RUST_TARGET }} + + + + - name: Install cross-compilation dependencies + run: | + sudo dpkg --add-architecture arm64 + # Configure sources for ARM64 packages - all ARM64 packages come from ports.ubuntu.com + sudo tee /etc/apt/sources.list.d/arm64.list > /dev/null <<'EOF' + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse + EOF + # Modify existing sources to exclude arm64 architecture + sudo sed -i 's/^deb /deb [arch=amd64] /' /etc/apt/sources.list + sudo apt-get update -y + # Install build tools and cross-compilation libraries for Raspberry Pi 5 + sudo apt-get install -y gcc-aarch64-linux-gnu libudev-dev:arm64 pkg-config cmake libudev-dev + # Ensure pkg-config can find ARM64 libraries + sudo apt-get install -y libpkgconf3:arm64 + - name: Install Rust dependencies + run: cargo fetch --target ${{ env.RUST_TARGET }} + - name: Build release binary + run: cargo build --release --target ${{ env.RUST_TARGET }} + env: + # Set linker for the target + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + # Configure pkg-config for cross-compilation + PKG_CONFIG_ALLOW_CROSS: 1 + PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig + PKG_CONFIG_LIBDIR: /usr/lib/aarch64-linux-gnu/pkgconfig + PKG_CONFIG_SYSROOT_DIR: / + PKG_CONFIG_ALLOW_SYSTEM_LIBS: 1 + PKG_CONFIG_ALLOW_SYSTEM_CFLAGS: 1 + # Add library path for the cross-compiler's linker + RUSTFLAGS: -L/usr/lib/aarch64-linux-gnu + - name: Run tests on native platform + run: cargo test --release --bin ntp_timeturner + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: timeturner-aarch64 + path: target/${{ env.RUST_TARGET }}/release/ntp_timeturner diff --git a/.gitignore b/.gitignore index 9491a2f..0c5fc70 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,21 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +.aider* +# Generated by Cargo +# will have compiled files and executables +debug +target +# These are backup files generated by rustfmt +**/*.rs.bk +# MSVC Windows builds of rustc generate these, which store debugging information +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Cargo.toml b/Cargo.toml index fc040de..8f7f726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" [dependencies] serialport = "4.2" chrono = "0.4" -crossterm = "0.27" +crossterm = "0.29" regex = "1.11" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -notify = "5.1.0" \ No newline at end of file +notify = "8.1.0" \ No newline at end of file diff --git a/README.md b/README.md index 738f128..1d48356 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,13 @@ Inspired by the TimeTurner in the Harry Potter series, this project synchronises --- -## 🚀 Installation +## 🚀 Installation (to update) + +For Rust install you can do +```bash +cargo install --git https://github.com/cjfranko/NTP-Timeturner +``` Clone and run the installer: ```bash diff --git a/src/sync_logic.rs b/src/sync_logic.rs index 3b9f435..e985441 100644 --- a/src/sync_logic.rs +++ b/src/sync_logic.rs @@ -135,3 +135,72 @@ impl LtcState { &self.last_match_status } } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Local, Utc}; + + fn get_test_frame(status: &str, h: u32, m: u32, s: u32) -> LtcFrame { + LtcFrame { + status: status.to_string(), + hours: h, + minutes: m, + seconds: s, + frames: 0, + frame_rate: 25.0, + timestamp: Utc::now(), + } + } + + #[test] + fn test_ltc_frame_matches_system_time() { + let now = Local::now(); + let frame = get_test_frame("LOCK", now.hour(), now.minute(), now.second()); + assert!(frame.matches_system_time()); + } + + #[test] + fn test_ltc_frame_does_not_match_system_time() { + let now = Local::now(); + // Create a time that is one hour ahead, wrapping around 23:00 + let different_hour = (now.hour() + 1) % 24; + let frame = get_test_frame("LOCK", different_hour, now.minute(), now.second()); + assert!(!frame.matches_system_time()); + } + + #[test] + fn test_ltc_state_update_lock() { + let mut state = LtcState::new(); + let frame = get_test_frame("LOCK", 10, 20, 30); + state.update(frame); + assert_eq!(state.lock_count, 1); + assert_eq!(state.free_count, 0); + assert!(state.latest.is_some()); + } + + #[test] + fn test_ltc_state_update_free() { + let mut state = LtcState::new(); + state.record_offset(100); + assert!(!state.offset_history.is_empty()); + + let frame = get_test_frame("FREE", 10, 20, 30); + state.update(frame); + assert_eq!(state.lock_count, 0); + assert_eq!(state.free_count, 1); + assert!(state.offset_history.is_empty()); // Offsets should be cleared + assert_eq!(state.last_match_status, "UNKNOWN"); + } + + #[test] + fn test_offset_history_management() { + let mut state = LtcState::new(); + for i in 0..25 { + state.record_offset(i); + } + assert_eq!(state.offset_history.len(), 20); + assert_eq!(*state.offset_history.front().unwrap(), 5); // 0-4 are pushed out + assert_eq!(*state.offset_history.back().unwrap(), 24); + } +} \ No newline at end of file