vfo-sync

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

main.cpp (9300B)


      1 #include <imgui.h>
      2 #include <module.h>
      3 #include <core.h>
      4 #include <gui/gui.h>
      5 #include <gui/tuner.h>
      6 #include <signal_path/signal_path.h>
      7 #include <utils/flog.h>
      8 #include <radio_interface.h>
      9 
     10 #include <cstring>
     11 #include <cstdint>
     12 #include <algorithm>
     13 #include <mutex>
     14 #include <string>
     15 #include <vector>
     16 
     17 extern "C" {
     18     void* civ_create(const char* name);
     19     void civ_destroy(void* ctx);
     20     void civ_post_init(void* ctx);
     21     void civ_enable(void* ctx);
     22     void civ_disable(void* ctx);
     23     bool civ_is_enabled(void* ctx);
     24     void civ_status(void* ctx, char* out, size_t out_len);
     25     void civ_set_serial_port(void* ctx, const char* port);
     26     void civ_set_serial_baud(void* ctx, uint32_t baud);
     27     void civ_set_sync_mode(void* ctx, bool sync_mode);
     28     size_t civ_list_serial_ports(char* out, size_t out_len);
     29 }
     30 
     31 SDRPP_MOD_INFO{
     32     /* Name:            */ "civ_sync",
     33     /* Description:     */ "Sync an Icom IC-7300 (CI-V) with an SDR++ VFO (freq/mode/filter)",
     34     /* Author:          */ "vfo-sync",
     35     /* Version:         */ 0, 2, 0,
     36     /* Max instances    */ 1
     37 };
     38 
     39 // Guard the GUI-side state accessors (waterfall/VFO) called off-thread.
     40 static std::mutex guiMtx;
     41 
     42 // Returns the selected VFO name if it is bound to the "radio" demodulator,
     43 // or "" otherwise. Caller must hold guiMtx.
     44 static std::string selectedRadioVfo() {
     45     std::string vfo = gui::waterfall.selectedVFO;
     46     if (!core::modComManager.interfaceExists(vfo) ||
     47         core::modComManager.getModuleName(vfo) != "radio") {
     48         return "";
     49     }
     50     return vfo;
     51 }
     52 
     53 extern "C" void civ_bridge_tune(double freq) {
     54     std::lock_guard<std::mutex> lck(guiMtx);
     55     tuner::tune(tuner::TUNER_MODE_NORMAL, gui::waterfall.selectedVFO, freq);
     56 }
     57 
     58 extern "C" double civ_bridge_get_vfo_freq() {
     59     std::lock_guard<std::mutex> lck(guiMtx);
     60     double freq = gui::waterfall.getCenterFrequency();
     61     if (sigpath::vfoManager.vfoExists(gui::waterfall.selectedVFO)) {
     62         freq += sigpath::vfoManager.getOffset(gui::waterfall.selectedVFO);
     63     }
     64     return freq;
     65 }
     66 
     67 extern "C" int civ_bridge_get_mode() {
     68     std::lock_guard<std::mutex> lck(guiMtx);
     69     std::string vfo = selectedRadioVfo();
     70     if (vfo.empty()) { return -1; }
     71     int mode = -1;
     72     core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_GET_MODE, NULL, &mode);
     73     return mode;
     74 }
     75 
     76 extern "C" void civ_bridge_set_mode(int mode) {
     77     std::lock_guard<std::mutex> lck(guiMtx);
     78     std::string vfo = selectedRadioVfo();
     79     if (vfo.empty()) { return; }
     80     core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_SET_MODE, &mode, NULL);
     81 }
     82 
     83 extern "C" double civ_bridge_get_bandwidth() {
     84     std::lock_guard<std::mutex> lck(guiMtx);
     85     std::string vfo = selectedRadioVfo();
     86     if (vfo.empty()) { return 0.0; }
     87     float bw = 0.0f;
     88     core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_GET_BANDWIDTH, NULL, &bw);
     89     return (double)bw;
     90 }
     91 
     92 extern "C" void civ_bridge_set_bandwidth(double hz) {
     93     std::lock_guard<std::mutex> lck(guiMtx);
     94     std::string vfo = selectedRadioVfo();
     95     if (vfo.empty()) { return; }
     96     float bw = (float)hz;
     97     core::modComManager.callInterface(vfo, RADIO_IFACE_CMD_SET_BANDWIDTH, &bw, NULL);
     98 }
     99 
    100 extern "C" void civ_bridge_log(int level, const char* msg) {
    101     if (msg == nullptr) { return; }
    102     switch (level) {
    103         case 0: flog::debug("{0}", msg); break;
    104         case 1: flog::info("{0}", msg); break;
    105         case 2: flog::warn("{0}", msg); break;
    106         default: flog::error("{0}", msg); break;
    107     }
    108 }
    109 
    110 class CivInstance : public ModuleManager::Instance {
    111 public:
    112     CivInstance(std::string name) {
    113         this->name = name;
    114         // Default to a detected port (preferring ttyUSB*) rather than a
    115         // hardcoded device that may not exist on this system.
    116         strncpy(portBuf, "/dev/ttyUSB0", sizeof(portBuf) - 1);
    117         refreshPorts();
    118         if (!ports.empty()) {
    119             auto it = std::find_if(ports.begin(), ports.end(), [](const std::string& p) {
    120                 return p.find("ttyUSB") != std::string::npos;
    121             });
    122             if (it == ports.end()) { it = ports.begin(); }
    123             strncpy(portBuf, it->c_str(), sizeof(portBuf) - 1);
    124             portBuf[sizeof(portBuf) - 1] = 0;
    125         }
    126         rustCtx = civ_create(name.c_str());
    127         civ_set_serial_port(rustCtx, portBuf);
    128         civ_set_serial_baud(rustCtx, baud);
    129         gui::menu.registerEntry(name, menuHandler, this, this);
    130     }
    131 
    132     ~CivInstance() {
    133         gui::menu.removeEntry(name);
    134         civ_destroy(rustCtx);
    135     }
    136 
    137     void postInit() override { civ_post_init(rustCtx); }
    138     void enable() override { civ_enable(rustCtx); }
    139     void disable() override { civ_disable(rustCtx); }
    140     bool isEnabled() override { return civ_is_enabled(rustCtx); }
    141 
    142 private:
    143     void refreshPorts() {
    144         ports.clear();
    145         char buf[4096] = {0};
    146         size_t n = civ_list_serial_ports(buf, sizeof(buf));
    147         const char* p = buf;
    148         for (size_t i = 0; i < n; i++) {
    149             std::string name(p);
    150             if (!name.empty()) { ports.push_back(name); }
    151             p += name.size() + 1;
    152         }
    153     }
    154 
    155     static void menuHandler(void* ctx) {
    156         CivInstance* _this = (CivInstance*)ctx;
    157 
    158         char statusBuf[256] = {0};
    159         civ_status(_this->rustCtx, statusBuf, sizeof(statusBuf));
    160         ImGui::TextUnformatted(statusBuf);
    161 
    162         std::string portPreview = _this->customPortMode
    163             ? "Custom (" + std::string(_this->portBuf) + ")"
    164             : std::string(_this->portBuf);
    165 
    166         if (ImGui::BeginCombo("Serial device", portPreview.c_str())) {
    167             _this->refreshPorts();
    168             for (const auto& p : _this->ports) {
    169                 bool selected = !_this->customPortMode && p == _this->portBuf;
    170                 if (ImGui::Selectable(p.c_str(), selected)) {
    171                     strncpy(_this->portBuf, p.c_str(), sizeof(_this->portBuf) - 1);
    172                     _this->portBuf[sizeof(_this->portBuf) - 1] = 0;
    173                     _this->customPortMode = false;
    174                     civ_set_serial_port(_this->rustCtx, _this->portBuf);
    175                 }
    176             }
    177             if (ImGui::Selectable("Custom...", _this->customPortMode)) {
    178                 _this->customPortMode = true;
    179             }
    180             ImGui::EndCombo();
    181         }
    182 
    183         if (_this->customPortMode) {
    184             if (ImGui::InputText("Custom device", _this->portBuf, sizeof(_this->portBuf))) {
    185                 civ_set_serial_port(_this->rustCtx, _this->portBuf);
    186             }
    187         }
    188 
    189         static const uint32_t baudRates[] = { 0, 4800, 9600, 19200, 38400, 57600, 115200 };
    190         static const char* baudNames[] = { "Auto", "4800", "9600", "19200", "38400", "57600", "115200" };
    191         static const int baudCount = 7;
    192 
    193         int selected = -1;
    194         for (int i = 0; i < baudCount; i++) {
    195             if (!_this->customBaudMode && _this->baud == baudRates[i]) { selected = i; break; }
    196         }
    197 
    198         std::string preview;
    199         if (_this->customBaudMode) {
    200             preview = "Custom (" + std::to_string(_this->baud) + ")";
    201         }
    202         else if (selected >= 0) {
    203             preview = baudNames[selected];
    204         }
    205         else {
    206             preview = std::to_string(_this->baud);
    207         }
    208 
    209         if (ImGui::BeginCombo("Baud rate", preview.c_str())) {
    210             for (int i = 0; i < baudCount; i++) {
    211                 if (ImGui::Selectable(baudNames[i], !_this->customBaudMode && selected == i)) {
    212                     _this->customBaudMode = false;
    213                     _this->baud = baudRates[i];
    214                     civ_set_serial_baud(_this->rustCtx, _this->baud);
    215                 }
    216             }
    217             if (ImGui::Selectable("Custom...", _this->customBaudMode)) {
    218                 _this->customBaudMode = true;
    219                 _this->customBaud = _this->baud > 0 ? (int)_this->baud : 115200;
    220                 _this->baud = (uint32_t)_this->customBaud;
    221                 civ_set_serial_baud(_this->rustCtx, _this->baud);
    222             }
    223             ImGui::EndCombo();
    224         }
    225 
    226         if (_this->customBaudMode) {
    227             if (ImGui::InputInt("Custom baud", &_this->customBaud)) {
    228                 if (_this->customBaud > 0) {
    229                     _this->baud = (uint32_t)_this->customBaud;
    230                     civ_set_serial_baud(_this->rustCtx, _this->baud);
    231                 }
    232             }
    233         }
    234 
    235         if (ImGui::Checkbox("Sync mode & filter", &_this->syncMode)) {
    236             civ_set_sync_mode(_this->rustCtx, _this->syncMode);
    237         }
    238 
    239         if (ImGui::Button(_this->isEnabled() ? "Disconnect" : "Connect")) {
    240             if (_this->isEnabled()) {
    241                 _this->disable();
    242             }
    243             else {
    244                 _this->enable();
    245             }
    246         }
    247     }
    248 
    249     std::string name;
    250     void* rustCtx = nullptr;
    251     char portBuf[256] = {0};
    252     std::vector<std::string> ports;
    253     bool customPortMode = false;
    254     uint32_t baud = 0;
    255     bool customBaudMode = false;
    256     int customBaud = 115200;
    257     bool syncMode = true;
    258 };
    259 
    260 MOD_EXPORT void _INIT_() {
    261     // Nothing here
    262 }
    263 
    264 MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
    265     return new CivInstance(name);
    266 }
    267 
    268 MOD_EXPORT void _DELETE_INSTANCE_(void* instance) {
    269     delete (CivInstance*)instance;
    270 }
    271 
    272 MOD_EXPORT void _END_() {
    273     // Nothing here
    274 }