vfo-sync

Bidirectional VFO synchronization for IC-7300 and SDR++
git clone https://git.bradleygannon.com/vfo-sync.git
Log | Files | Refs

commit 038f4f471bc5b4a81cb907389d87b4255381b6bb
parent fa35643a5cf7655f67d065ca1bfd6e51ca64fd48
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Tue,  1 Sep 2026 19:21:24 -0400

Add initial SDR++ module with Rust internals

Diffstat:
M.gitignore | 3+++
Aciv_module/Cargo.lock | 7+++++++
Aciv_module/Cargo.toml | 9+++++++++
Aciv_module/Makefile | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aciv_module/shim/CMakeLists.txt | 41+++++++++++++++++++++++++++++++++++++++++
Aciv_module/shim/src/main.cpp | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aciv_module/src/lib.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mflake.nix | 15++++++++++++---
8 files changed, 290 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1 +1,4 @@ /target +/civ_module/target +/civ_module/build +/civ_module/dist diff --git a/civ_module/Cargo.lock b/civ_module/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "civ_sync" +version = "0.1.0" diff --git a/civ_module/Cargo.toml b/civ_module/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "civ_sync" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] diff --git a/civ_module/Makefile b/civ_module/Makefile @@ -0,0 +1,72 @@ +# Builds the civ_sync SDR++ module (Rust staticlib + C++ shim) and installs the +# resulting .so into a local SDR++ config root for GUI loading. +# +# The SDR++ source location is configurable via the SDRPP_DIR environment +# variable or `make SDRPP_DIR=...`; it defaults to ../.hidden/SDRPlusPlus. +# +# Run inside `nix develop` so cargo, cmake, g++, and the SDR++ deps are present. + +SHELL := bash + +CIV_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +REPO_ROOT := $(abspath $(CIV_DIR)/..) + +SDRPP_DIR ?= $(abspath $(REPO_ROOT)/.hidden/SDRPlusPlus) +SDRPP_BUILD := $(SDRPP_DIR)/build +CORE_LIB := $(SDRPP_BUILD)/core/libsdrpp_core.so + +MOD_BUILD := $(CIV_DIR)/build +MODULE_SO := $(MOD_BUILD)/civ_sync.so + +DIST_ROOT ?= $(CIV_DIR)/dist/root +MODULES_DIR ?= $(DIST_ROOT)/modules +INSTALLED_SO := $(MODULES_DIR)/civ_sync.so + +NPROC ?= $(shell nproc 2>/dev/null || echo 4) + +.PHONY: all module rust core install clean help + +all: module install + +module: rust $(CORE_LIB) + @echo "==> Configuring civ_sync module" + cmake -S "$(CIV_DIR)/shim" -B "$(MOD_BUILD)" \ + -DCMAKE_BUILD_TYPE=Release -DSDRPP_DIR="$(SDRPP_DIR)" + @echo "==> Building civ_sync module" + cmake --build "$(MOD_BUILD)" -j$(NPROC) + +rust: + @echo "==> Building Rust staticlib" + cargo build --release --manifest-path "$(CIV_DIR)/Cargo.toml" + +$(CORE_LIB): + @echo "==> Configuring SDR++ (core only)" + @opts=""; for opt in $$(grep -oE 'option\(OPT_BUILD_[A-Z0-9_]+' "$(SDRPP_DIR)/CMakeLists.txt" | sed 's/option(//' | sort -u); do opts="$${opts} -D$${opt}=OFF"; done; \ + cmake -S "$(SDRPP_DIR)" -B "$(SDRPP_BUILD)" -DCMAKE_BUILD_TYPE=Release $${opts} + @echo "==> Building sdrpp_core" + cmake --build "$(SDRPP_BUILD)" --target sdrpp_core -j$(NPROC) + +core: $(CORE_LIB) + +install: module + @echo "==> Installing module to $(MODULES_DIR)" + mkdir -p "$(MODULES_DIR)" + cp -f "$(MODULE_SO)" "$(INSTALLED_SO)" + +clean: + rm -rf "$(MOD_BUILD)" "$(DIST_ROOT)" + +help: + @printf '%s\n' \ + 'Targets:' \ + ' all build module + install into $(MODULES_DIR) (default)' \ + ' module build $(MODULE_SO)' \ + ' rust build the Rust staticlib' \ + ' core build the SDR++ core library only' \ + ' install copy $(MODULE_SO) into $(MODULES_DIR)' \ + ' clean remove module build dir and installed output' \ + '' \ + 'Variables (env or `make VAR=...`):' \ + ' SDRPP_DIR SDR++ source root [default: $(SDRPP_DIR)]' \ + ' MODULES_DIR module install dir [default: $(MODULES_DIR)]' \ + ' NPROC parallel jobs [default: $(NPROC)]' diff --git a/civ_module/shim/CMakeLists.txt b/civ_module/shim/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.13) +project(civ_sync) + +set(SDRPP_DIR "" CACHE PATH "Path to the SDR++ source root") +if (NOT SDRPP_DIR) + message(FATAL_ERROR "SDRPP_DIR must be set to the SDR++ source root") +endif() + +set(CIV_STATICLIB "${CMAKE_CURRENT_SOURCE_DIR}/../target/release/libciv_sync.a" + CACHE FILEPATH "Path to the built Rust staticlib") +if (NOT EXISTS "${CIV_STATICLIB}") + message(FATAL_ERROR "Rust staticlib not found at ${CIV_STATICLIB}; run 'cargo build --release' first") +endif() + +set(SDRPP_CORE_LIB "${SDRPP_DIR}/build/core/libsdrpp_core.so" + CACHE FILEPATH "Path to the built libsdrpp_core.so") +if (NOT EXISTS "${SDRPP_CORE_LIB}") + message(FATAL_ERROR "libsdrpp_core.so not found at ${SDRPP_CORE_LIB}; build the SDR++ core first") +endif() + +find_package(OpenGL REQUIRED) + +file(GLOB SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") + +add_library(${PROJECT_NAME} SHARED ${SRC}) + +target_include_directories(${PROJECT_NAME} PRIVATE + "${SDRPP_DIR}/core/src" + "${SDRPP_DIR}/core/src/imgui" + ${OPENGL_INCLUDE_DIRS} +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + "${SDRPP_CORE_LIB}" + "${CIV_STATICLIB}" + pthread dl m +) + +target_compile_options(${PROJECT_NAME} PRIVATE -std=c++17 -O2 -fPIC) + +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") diff --git a/civ_module/shim/src/main.cpp b/civ_module/shim/src/main.cpp @@ -0,0 +1,65 @@ +#include <imgui.h> +#include <module.h> +#include <gui/gui.h> + +extern "C" { + void* civ_create(const char* name); + void civ_destroy(void* ctx); + void civ_post_init(void* ctx); + void civ_enable(void* ctx); + void civ_disable(void* ctx); + bool civ_is_enabled(void* ctx); + const char* civ_status(void* ctx); +} + +SDRPP_MOD_INFO{ + /* Name: */ "civ_sync", + /* Description: */ "Sync an Icom IC-7300 (CI-V) with an SDR++ VFO", + /* Author: */ "vfo-sync", + /* Version: */ 0, 1, 0, + /* Max instances */ 1 +}; + +class CivInstance : public ModuleManager::Instance { +public: + CivInstance(std::string name) { + this->name = name; + rustCtx = civ_create(name.c_str()); + gui::menu.registerEntry(name, menuHandler, this, this); + } + + ~CivInstance() { + gui::menu.removeEntry(name); + civ_destroy(rustCtx); + } + + void postInit() override { civ_post_init(rustCtx); } + void enable() override { civ_enable(rustCtx); } + void disable() override { civ_disable(rustCtx); } + bool isEnabled() override { return civ_is_enabled(rustCtx); } + +private: + static void menuHandler(void* ctx) { + CivInstance* _this = (CivInstance*)ctx; + ImGui::TextUnformatted(civ_status(_this->rustCtx)); + } + + std::string name; + void* rustCtx = nullptr; +}; + +MOD_EXPORT void _INIT_() { + // Nothing here +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new CivInstance(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (CivInstance*)instance; +} + +MOD_EXPORT void _END_() { + // Nothing here +} diff --git a/civ_module/src/lib.rs b/civ_module/src/lib.rs @@ -0,0 +1,81 @@ +#![allow(clippy::missing_safety_doc)] + +use std::ffi::{c_char, CStr, CString}; + +pub struct CivCtx { + name: String, + status: CString, + enabled: bool, +} + +#[no_mangle] +pub unsafe extern "C" fn civ_create(name: *const c_char) -> *mut CivCtx { + let name = if name.is_null() { + "<unnamed>".to_string() + } else { + unsafe { CStr::from_ptr(name) } + .to_string_lossy() + .into_owned() + }; + + let msg = format!("Rust core alive (instance: {})", name); + eprintln!("[civ_sync] {}", msg); + + let ctx = CivCtx { + name, + status: CString::new(msg).unwrap(), + enabled: false, + }; + Box::into_raw(Box::new(ctx)) +} + +#[no_mangle] +pub unsafe extern "C" fn civ_destroy(ctx: *mut CivCtx) { + if ctx.is_null() { + return; + } + let ctx = Box::from_raw(ctx); + eprintln!("[civ_sync] Rust core destroyed (instance: {})", ctx.name); +} + +#[no_mangle] +pub unsafe extern "C" fn civ_post_init(ctx: *mut CivCtx) { + if ctx.is_null() { + return; + } + eprintln!("[civ_sync] Rust core post-init (instance: {})", (*ctx).name); +} + +#[no_mangle] +pub unsafe extern "C" fn civ_enable(ctx: *mut CivCtx) { + if ctx.is_null() { + return; + } + (*ctx).enabled = true; + eprintln!("[civ_sync] Rust core enabled (instance: {})", (*ctx).name); +} + +#[no_mangle] +pub unsafe extern "C" fn civ_disable(ctx: *mut CivCtx) { + if ctx.is_null() { + return; + } + (*ctx).enabled = false; + eprintln!("[civ_sync] Rust core disabled (instance: {})", (*ctx).name); +} + +#[no_mangle] +pub unsafe extern "C" fn civ_is_enabled(ctx: *mut CivCtx) -> bool { + if ctx.is_null() { + return false; + } + (*ctx).enabled +} + +#[no_mangle] +pub unsafe extern "C" fn civ_status(ctx: *mut CivCtx) -> *const c_char { + if ctx.is_null() { + return std::ptr::null(); + } + (*ctx).status.as_ptr() +} diff --git a/flake.nix b/flake.nix @@ -12,7 +12,14 @@ config = { }; overlays = [ ]; }; - libInputs = [ ]; + libInputs = with stable; [ + fftwFloat + glfw + libGL + stdenv.cc.cc + volk + zstd + ]; lib-path = with stable; lib.makeLibraryPath libInputs; in { @@ -20,13 +27,15 @@ packages = with stable; [ cargo clippy + cmake + gcc + pkg-config rust-analyzer rustc rustfmt - pkg-config systemd udev - ]; + ] ++ libInputs; shellHook = '' export "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${lib-path}"