mirror of
https://github.com/RFnexus/modem73.git
synced 2026-04-27 14:30:33 +00:00
Init
This commit is contained in:
commit
f58acebeb1
13 changed files with 101862 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
miniaudio.o
|
||||||
24
LICENSE
Normal file
24
LICENSE
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org>
|
||||||
62
Makefile
Normal file
62
Makefile
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
CXX = g++
|
||||||
|
CXXFLAGS = -std=c++17 -O3 -march=native -Wall -Wextra
|
||||||
|
LDFLAGS = -lpthread -lncurses -ldl -lm
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
AICODIX_DSP ?= ../dsp
|
||||||
|
AICODIX_CODE ?= ../code
|
||||||
|
MODEM_SRC ?= ../modem
|
||||||
|
|
||||||
|
INCLUDES = -I$(AICODIX_DSP) -I$(AICODIX_CODE) -I$(MODEM_SRC)
|
||||||
|
|
||||||
|
TARGET = modem73
|
||||||
|
|
||||||
|
SRCS = kiss_tnc.cc
|
||||||
|
HDRS = kiss_tnc.hh miniaudio_audio.hh rigctl_ptt.hh modem.hh tnc_ui.hh
|
||||||
|
OBJS = miniaudio.o
|
||||||
|
|
||||||
|
# defualt to build with UI, headless operations through --headless
|
||||||
|
UI_FLAGS = -DWITH_UI
|
||||||
|
|
||||||
|
.PHONY: all clean install debug help
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
miniaudio.o: miniaudio.c miniaudio.h
|
||||||
|
$(CC) -c -O2 -o $@ miniaudio.c
|
||||||
|
|
||||||
|
$(TARGET): $(SRCS) $(HDRS) $(OBJS)
|
||||||
|
$(CXX) $(CXXFLAGS) $(UI_FLAGS) $(INCLUDES) -o $@ $(SRCS) $(OBJS) $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(TARGET) $(OBJS)
|
||||||
|
|
||||||
|
install: $(TARGET)
|
||||||
|
install -m 755 $(TARGET) /usr/local/bin/
|
||||||
|
|
||||||
|
# Debug build
|
||||||
|
debug: CXXFLAGS = -std=c++17 -g -O0 -Wall -Wextra -DDEBUG
|
||||||
|
debug: $(TARGET)
|
||||||
|
|
||||||
|
# Help
|
||||||
|
help:
|
||||||
|
@echo "MODEM73 makefile"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " all - Build modem"
|
||||||
|
@echo " clean - Remove build"
|
||||||
|
@echo " install - Install to /usr/local/bin"
|
||||||
|
@echo " debug - Build with debug symbols"
|
||||||
|
@echo ""
|
||||||
|
@echo "Variables:"
|
||||||
|
@echo " AICODIX_DSP - Path to aicodix/dsp (default: ../dsp)"
|
||||||
|
@echo " AICODIX_CODE - Path to aicodix/code (default: ../code)"
|
||||||
|
@echo " MODEM_SRC - Path to modem source (default: ../modem)"
|
||||||
|
@echo ""
|
||||||
|
@echo "Example:"
|
||||||
|
@echo " make AICODIX_DSP=~/aicodix/dsp AICODIX_CODE=~/aicodix/code"
|
||||||
|
@echo ""
|
||||||
|
@echo "Runtime options:"
|
||||||
|
@echo " ./modem73 # Run with UI"
|
||||||
|
@echo " ./modem73 -h # Run headless"
|
||||||
|
@echo " ./modem73 --headless"
|
||||||
101
README.md
Normal file
101
README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/LDNR23jg/MODEM73-white.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://i.ibb.co/wZKznzrF/MODEM73-blk.png">
|
||||||
|
<img alt="MODEM73" src="https://i.ibb.co/wZKznzrF/MODEM73-blk.png">
|
||||||
|
</picture>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
MODEM73 is a [KISS](https://en.wikipedia.org/wiki/KISS_(amateur_radio_protocol)) TNC frontend for the [aicodix](https://github.com/aicodix/modem) OFDM modem.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
|
||||||
|
1. Install dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
# Debian/Ubuntu/Pi
|
||||||
|
sudo apt install git build-essential libncurses-dev g++
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Clone aiocdix DSP libraries and build.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
# Requires DSP, code, and modem libraries
|
||||||
|
git clone https://github.com/aicodix/dsp.git
|
||||||
|
git clone https://github.com/aicodix/code.git
|
||||||
|
git clone https://github.com/aicodix/modem.git
|
||||||
|
|
||||||
|
# Clone modem73
|
||||||
|
git clone https://github.com/RFnexus/modem73
|
||||||
|
|
||||||
|
# Your folders should look like this:
|
||||||
|
#.../
|
||||||
|
#├── dsp/ # DSP library (aicodix)
|
||||||
|
#│ └── ...
|
||||||
|
#├── code/ # Code library (aicodix)
|
||||||
|
#│ └── ...
|
||||||
|
#├── modem/ # Modem library (aicodix)
|
||||||
|
#│ └── ...
|
||||||
|
#└── modem73/ # modem73 src
|
||||||
|
# └── ...
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cd modem73
|
||||||
|
make AICODIX_DSP=../dsp AICODIX_CODE=../code MODEM_SRC=../modem
|
||||||
|
# Optional: move to /usr/local/bin
|
||||||
|
sudo make install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running & Operations
|
||||||
|
|
||||||
|
By default, MODEM73 will listen on port 8001
|
||||||
|
|
||||||
|
All of the modes provided by the OFDM modem require a bandwidth of 2400 Hz and work over both FM and SSB.
|
||||||
|
|
||||||
|
There are currently four PTT options:
|
||||||
|
- NONE (speaker/mic over the air)
|
||||||
|
- Rigctl
|
||||||
|
- VOX
|
||||||
|
- Serial
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
# Start in UI mode
|
||||||
|
./modem73
|
||||||
|
|
||||||
|
# Start in headless mode
|
||||||
|
./modem73 --headless
|
||||||
|
|
||||||
|
# See all options with:
|
||||||
|
./modem73 --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### PTT options
|
||||||
|
|
||||||
|
```
|
||||||
|
# Connect to rigctld for PTT control
|
||||||
|
./modem73 --rigctl localhost:4532
|
||||||
|
```
|
||||||
|
|
||||||
|
while running `rigctld`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
./modem73 --ptt vox --vox-freq 1200 --vox-lead 500 --vox-tail 150
|
||||||
|
# 500ms vox lead and 150ms vox tail
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
./modem73 --ptt com --com-port /dev/ttyUSB0 --com-line rts
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1192
kiss_tnc.cc
Normal file
1192
kiss_tnc.cc
Normal file
File diff suppressed because it is too large
Load diff
248
kiss_tnc.hh
Normal file
248
kiss_tnc.hh
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include <queue>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// KISS protocol
|
||||||
|
namespace KISS {
|
||||||
|
constexpr uint8_t FEND = 0xC0;
|
||||||
|
constexpr uint8_t FESC = 0xDB;
|
||||||
|
constexpr uint8_t TFEND = 0xDC;
|
||||||
|
constexpr uint8_t TFESC = 0xDD;
|
||||||
|
|
||||||
|
// KISS commands
|
||||||
|
constexpr uint8_t CMD_DATA = 0x00;
|
||||||
|
constexpr uint8_t CMD_TXDELAY = 0x01;
|
||||||
|
constexpr uint8_t CMD_P = 0x02;
|
||||||
|
constexpr uint8_t CMD_SLOTTIME = 0x03;
|
||||||
|
constexpr uint8_t CMD_TXTAIL = 0x04;
|
||||||
|
constexpr uint8_t CMD_FULLDUPLEX = 0x05;
|
||||||
|
constexpr uint8_t CMD_SETHW = 0x06;
|
||||||
|
constexpr uint8_t CMD_RETURN = 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class PTTType {
|
||||||
|
NONE = 0,
|
||||||
|
RIGCTL = 1,
|
||||||
|
VOX = 2,
|
||||||
|
COM = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TNCConfig {
|
||||||
|
// Network settings
|
||||||
|
std::string bind_address = "0.0.0.0";
|
||||||
|
int port = 8001;
|
||||||
|
|
||||||
|
// Audio settings
|
||||||
|
std::string audio_input_device = "default";
|
||||||
|
std::string audio_output_device = "default";
|
||||||
|
int sample_rate = 48000;
|
||||||
|
|
||||||
|
// Modem settings
|
||||||
|
int center_freq = 1500;
|
||||||
|
std::string callsign = "N0CALL";
|
||||||
|
std::string modulation = "QPSK";
|
||||||
|
std::string code_rate = "1/2";
|
||||||
|
bool short_frame = false;
|
||||||
|
|
||||||
|
// PTT settings
|
||||||
|
PTTType ptt_type = PTTType::RIGCTL;
|
||||||
|
|
||||||
|
// Rigctl settings
|
||||||
|
std::string rigctl_host = "localhost";
|
||||||
|
int rigctl_port = 4532;
|
||||||
|
|
||||||
|
// VOX settings
|
||||||
|
int vox_tone_freq = 1200; // Hz - tone frequency for VOX trigger
|
||||||
|
int vox_lead_ms = 550; // ms - tone before OFDM data
|
||||||
|
int vox_tail_ms = 500; // ms - tone after OFDM data
|
||||||
|
|
||||||
|
// COM/Serial PTT settings
|
||||||
|
std::string com_port = "/dev/ttyUSB0";
|
||||||
|
int com_ptt_line = 1; // 0=DTR, 1=RTS, 2=BOTH
|
||||||
|
bool com_invert_dtr = false;
|
||||||
|
bool com_invert_rts = false;
|
||||||
|
|
||||||
|
// PTT timing
|
||||||
|
int ptt_delay_ms = 50; // Delay after PTT before TX
|
||||||
|
int ptt_tail_ms = 50; // Delay after TX before PTT release
|
||||||
|
|
||||||
|
// Operational settings
|
||||||
|
int tx_delay_ms = 500; // TXDelay
|
||||||
|
bool full_duplex = false;
|
||||||
|
int slot_time_ms = 500; // CSMA slot time
|
||||||
|
int p_persistence = 128; // 0-255 (128 defualt 50%)
|
||||||
|
|
||||||
|
// CSMA settings
|
||||||
|
bool csma_enabled = true;
|
||||||
|
float carrier_threshold_db = -30.0f; // Carrier sense threshold
|
||||||
|
int carrier_sense_ms = 100; // How long to listen
|
||||||
|
int max_backoff_slots = 10; // Maximum backoff
|
||||||
|
|
||||||
|
// Settings file path
|
||||||
|
std::string config_file = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class KISSParser {
|
||||||
|
public:
|
||||||
|
using FrameCallback = std::function<void(uint8_t port, uint8_t cmd, const std::vector<uint8_t>&)>;
|
||||||
|
|
||||||
|
KISSParser(FrameCallback callback) : callback_(callback) {}
|
||||||
|
|
||||||
|
void process(const uint8_t* data, size_t len) {
|
||||||
|
for (size_t i = 0; i < len; ++i) {
|
||||||
|
process_byte(data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<uint8_t> wrap(const std::vector<uint8_t>& data, uint8_t port = 0) {
|
||||||
|
std::vector<uint8_t> frame;
|
||||||
|
frame.push_back(KISS::FEND);
|
||||||
|
frame.push_back((port << 4) | KISS::CMD_DATA);
|
||||||
|
|
||||||
|
for (uint8_t byte : data) {
|
||||||
|
if (byte == KISS::FEND) {
|
||||||
|
frame.push_back(KISS::FESC);
|
||||||
|
frame.push_back(KISS::TFEND);
|
||||||
|
} else if (byte == KISS::FESC) {
|
||||||
|
frame.push_back(KISS::FESC);
|
||||||
|
frame.push_back(KISS::TFESC);
|
||||||
|
} else {
|
||||||
|
frame.push_back(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.push_back(KISS::FEND);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void process_byte(uint8_t byte) {
|
||||||
|
if (byte == KISS::FEND) {
|
||||||
|
if (in_frame_ && buffer_.size() > 0) {
|
||||||
|
// frame complete
|
||||||
|
uint8_t cmd_byte = buffer_[0];
|
||||||
|
uint8_t port = (cmd_byte >> 4) & 0x0F;
|
||||||
|
uint8_t cmd = cmd_byte & 0x0F;
|
||||||
|
std::vector<uint8_t> payload(buffer_.begin() + 1, buffer_.end());
|
||||||
|
callback_(port, cmd, payload);
|
||||||
|
}
|
||||||
|
in_frame_ = true;
|
||||||
|
buffer_.clear();
|
||||||
|
escape_ = false;
|
||||||
|
} else if (in_frame_) {
|
||||||
|
if (escape_) {
|
||||||
|
if (byte == KISS::TFEND) {
|
||||||
|
buffer_.push_back(KISS::FEND);
|
||||||
|
} else if (byte == KISS::TFESC) {
|
||||||
|
buffer_.push_back(KISS::FESC);
|
||||||
|
} else {
|
||||||
|
buffer_.push_back(byte);
|
||||||
|
}
|
||||||
|
escape_ = false;
|
||||||
|
} else if (byte == KISS::FESC) {
|
||||||
|
escape_ = true;
|
||||||
|
} else {
|
||||||
|
buffer_.push_back(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameCallback callback_;
|
||||||
|
std::vector<uint8_t> buffer_;
|
||||||
|
bool in_frame_ = false;
|
||||||
|
bool escape_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
class PacketQueue {
|
||||||
|
public:
|
||||||
|
void push(T item) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
queue_.push(std::move(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pop(T& item) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (queue_.empty()) return false;
|
||||||
|
item = std::move(queue_.front());
|
||||||
|
queue_.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return queue_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return queue_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
while (!queue_.empty()) queue_.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
std::queue<T> queue_;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
inline void hex_dump(const char* prefix, const uint8_t* data, size_t len) {
|
||||||
|
std::cerr << prefix << " (" << len << " bytes):" << std::endl;
|
||||||
|
for (size_t i = 0; i < len; i += 16) {
|
||||||
|
std::cerr << " " << std::hex << std::setfill('0') << std::setw(4) << i << ": ";
|
||||||
|
|
||||||
|
// Hex
|
||||||
|
for (size_t j = 0; j < 16 && i + j < len; ++j) {
|
||||||
|
std::cerr << std::setw(2) << (int)data[i + j] << " ";
|
||||||
|
}
|
||||||
|
for (size_t j = len - i; j < 16 && i + j >= len; ++j) {
|
||||||
|
std::cerr << " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASCII
|
||||||
|
std::cerr << " |";
|
||||||
|
for (size_t j = 0; j < 16 && i + j < len; ++j) {
|
||||||
|
char c = data[i + j];
|
||||||
|
std::cerr << (c >= 32 && c < 127 ? c : '.');
|
||||||
|
}
|
||||||
|
std::cerr << "|" << std::endl;
|
||||||
|
}
|
||||||
|
std::cerr << std::dec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length-prefix framing
|
||||||
|
// This handles OFDM frame padding where the payload is encoded within the 2 byte prefix
|
||||||
|
inline std::vector<uint8_t> frame_with_length(const std::vector<uint8_t>& data) {
|
||||||
|
std::vector<uint8_t> framed;
|
||||||
|
uint16_t len = data.size();
|
||||||
|
framed.push_back((len >> 8) & 0xFF); // high byte
|
||||||
|
framed.push_back(len & 0xFF); // low byte
|
||||||
|
framed.insert(framed.end(), data.begin(), data.end());
|
||||||
|
return framed;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<uint8_t> unframe_length(const uint8_t* data, size_t total_len) {
|
||||||
|
if (total_len < 2) return {};
|
||||||
|
uint16_t payload_len = (data[0] << 8) | data[1];
|
||||||
|
if (payload_len > total_len - 2) {
|
||||||
|
std::cerr << "Warning: length prefix " << payload_len
|
||||||
|
<< " exceeds available data " << (total_len - 2) << std::endl;
|
||||||
|
payload_len = total_len - 2;
|
||||||
|
}
|
||||||
|
return std::vector<uint8_t>(data + 2, data + 2 + payload_len);
|
||||||
|
}
|
||||||
7
miniaudio.c
Normal file
7
miniaudio.c
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#define MA_NO_DECODING
|
||||||
|
#define MA_NO_ENCODING
|
||||||
|
#define MA_NO_GENERATION
|
||||||
|
#define MA_NO_ENGINE
|
||||||
|
#define MA_NO_NODE_GRAPH
|
||||||
|
#define MINIAUDIO_IMPLEMENTATION
|
||||||
|
#include "miniaudio.h"
|
||||||
95649
miniaudio.h
Normal file
95649
miniaudio.h
Normal file
File diff suppressed because it is too large
Load diff
444
miniaudio_audio.hh
Normal file
444
miniaudio_audio.hh
Normal file
|
|
@ -0,0 +1,444 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define MA_NO_DECODING
|
||||||
|
#define MA_NO_ENCODING
|
||||||
|
#define MA_NO_GENERATION
|
||||||
|
#define MA_NO_ENGINE
|
||||||
|
#define MA_NO_NODE_GRAPH
|
||||||
|
#include "miniaudio.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
class MiniAudio {
|
||||||
|
public:
|
||||||
|
static std::vector<std::pair<std::string, std::string>> list_capture_devices() {
|
||||||
|
std::vector<std::pair<std::string, std::string>> result;
|
||||||
|
|
||||||
|
ma_context context;
|
||||||
|
if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) {
|
||||||
|
result.push_back({"default", "default - System Default"});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_device_info* playback_devices;
|
||||||
|
ma_uint32 playback_count;
|
||||||
|
ma_device_info* capture_devices;
|
||||||
|
ma_uint32 capture_count;
|
||||||
|
|
||||||
|
if (ma_context_get_devices(&context, &playback_devices, &playback_count,
|
||||||
|
&capture_devices, &capture_count) != MA_SUCCESS) {
|
||||||
|
ma_context_uninit(&context);
|
||||||
|
result.push_back({"default", "default - System Default"});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back({"default", "default - System Default"});
|
||||||
|
|
||||||
|
for (ma_uint32 i = 0; i < capture_count; i++) {
|
||||||
|
std::string id = std::to_string(i);
|
||||||
|
std::string name = capture_devices[i].name;
|
||||||
|
std::string desc = id + " - " + name;
|
||||||
|
result.push_back({id, desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_context_uninit(&context);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::pair<std::string, std::string>> list_playback_devices() {
|
||||||
|
std::vector<std::pair<std::string, std::string>> result;
|
||||||
|
|
||||||
|
ma_context context;
|
||||||
|
if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) {
|
||||||
|
result.push_back({"default", "default - System Default"});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_device_info* playback_devices;
|
||||||
|
ma_uint32 playback_count;
|
||||||
|
ma_device_info* capture_devices;
|
||||||
|
ma_uint32 capture_count;
|
||||||
|
|
||||||
|
if (ma_context_get_devices(&context, &playback_devices, &playback_count,
|
||||||
|
&capture_devices, &capture_count) != MA_SUCCESS) {
|
||||||
|
ma_context_uninit(&context);
|
||||||
|
result.push_back({"default", "default - System Default"}); // ====!====
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back({"default", "default - System Default"});
|
||||||
|
|
||||||
|
for (ma_uint32 i = 0; i < playback_count; i++) {
|
||||||
|
std::string id = std::to_string(i);
|
||||||
|
std::string name = playback_devices[i].name;
|
||||||
|
std::string desc = id + " - " + name;
|
||||||
|
result.push_back({id, desc});
|
||||||
|
}
|
||||||
|
|
||||||
|
ma_context_uninit(&context);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// temp
|
||||||
|
static std::vector<std::pair<std::string, std::string>> list_devices() {
|
||||||
|
return list_capture_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
MiniAudio(const std::string& capture_dev = "default",
|
||||||
|
const std::string& playback_dev = "default",
|
||||||
|
int sample_rate = 48000)
|
||||||
|
: capture_device_id_(capture_dev), playback_device_id_(playback_dev), sample_rate_(sample_rate) {
|
||||||
|
capture_buffer_.resize(RING_BUFFER_SIZE, 0.0f);
|
||||||
|
playback_buffer_.resize(RING_BUFFER_SIZE, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MiniAudio() {
|
||||||
|
close_capture();
|
||||||
|
close_playback();
|
||||||
|
|
||||||
|
if (context_initialized_) {
|
||||||
|
ma_context_uninit(&context_);
|
||||||
|
context_initialized_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& capture_device() const { return capture_device_id_; }
|
||||||
|
const std::string& playback_device() const { return playback_device_id_; }
|
||||||
|
|
||||||
|
void set_capture_device(const std::string& device) {
|
||||||
|
close_capture();
|
||||||
|
capture_device_id_ = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_playback_device(const std::string& device) {
|
||||||
|
close_playback();
|
||||||
|
playback_device_id_ = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool open_playback() {
|
||||||
|
if (!ensure_context()) return false;
|
||||||
|
|
||||||
|
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||||
|
config.playback.format = ma_format_f32;
|
||||||
|
config.playback.channels = 1;
|
||||||
|
config.sampleRate = sample_rate_;
|
||||||
|
config.dataCallback = playback_callback;
|
||||||
|
config.pUserData = this;
|
||||||
|
config.periodSizeInFrames = 480;
|
||||||
|
config.periods = 4;
|
||||||
|
|
||||||
|
if (playback_device_id_ != "default" && !playback_device_id_.empty()) {
|
||||||
|
int dev_index = std::atoi(playback_device_id_.c_str());
|
||||||
|
|
||||||
|
ma_device_info* playback_devices;
|
||||||
|
ma_uint32 playback_count;
|
||||||
|
ma_device_info* capture_devices;
|
||||||
|
ma_uint32 capture_count;
|
||||||
|
|
||||||
|
if (ma_context_get_devices(&context_, &playback_devices, &playback_count,
|
||||||
|
&capture_devices, &capture_count) == MA_SUCCESS) {
|
||||||
|
if (dev_index >= 0 && dev_index < (int)playback_count) {
|
||||||
|
stored_playback_id_ = playback_devices[dev_index].id;
|
||||||
|
config.playback.pDeviceID = &stored_playback_id_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ma_device_init(&context_, &config, &playback_device_) != MA_SUCCESS) {
|
||||||
|
std::cerr << "Failed to init playback device" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ma_device_start(&playback_device_) != MA_SUCCESS) {
|
||||||
|
std::cerr << "Failed to start playback device" << std::endl;
|
||||||
|
ma_device_uninit(&playback_device_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
playback_open_ = true;
|
||||||
|
std::cerr << "Playback: " << playback_device_.playback.name << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool open_capture() {
|
||||||
|
if (!ensure_context()) return false;
|
||||||
|
|
||||||
|
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
||||||
|
config.capture.format = ma_format_f32;
|
||||||
|
config.capture.channels = 1;
|
||||||
|
config.sampleRate = sample_rate_;
|
||||||
|
config.dataCallback = capture_callback;
|
||||||
|
config.pUserData = this;
|
||||||
|
config.periodSizeInFrames = 480;
|
||||||
|
config.periods = 4;
|
||||||
|
|
||||||
|
if (capture_device_id_ != "default" && !capture_device_id_.empty()) {
|
||||||
|
int dev_index = std::atoi(capture_device_id_.c_str());
|
||||||
|
|
||||||
|
ma_device_info* playback_devices;
|
||||||
|
ma_uint32 playback_count;
|
||||||
|
ma_device_info* capture_devices;
|
||||||
|
ma_uint32 capture_count;
|
||||||
|
|
||||||
|
if (ma_context_get_devices(&context_, &playback_devices, &playback_count,
|
||||||
|
&capture_devices, &capture_count) == MA_SUCCESS) {
|
||||||
|
if (dev_index >= 0 && dev_index < (int)capture_count) {
|
||||||
|
stored_capture_id_ = capture_devices[dev_index].id;
|
||||||
|
config.capture.pDeviceID = &stored_capture_id_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ma_device_init(&context_, &config, &capture_device_) != MA_SUCCESS) {
|
||||||
|
std::cerr << "Failed to initialize capture device" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ma_device_start(&capture_device_) != MA_SUCCESS) {
|
||||||
|
std::cerr << "Failed to start capture device" << std::endl;
|
||||||
|
ma_device_uninit(&capture_device_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_open_ = true;
|
||||||
|
std::cerr << "Capture: " << capture_device_.capture.name << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close_playback() {
|
||||||
|
if (playback_open_) {
|
||||||
|
ma_device_uninit(&playback_device_);
|
||||||
|
playback_open_ = false;
|
||||||
|
}
|
||||||
|
playback_read_pos_ = 0;
|
||||||
|
playback_write_pos_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close_capture() {
|
||||||
|
if (capture_open_) {
|
||||||
|
ma_device_uninit(&capture_device_);
|
||||||
|
capture_open_ = false;
|
||||||
|
}
|
||||||
|
capture_read_pos_ = 0;
|
||||||
|
capture_write_pos_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int read(float* buffer, int frames) {
|
||||||
|
if (!capture_open_) return -1;
|
||||||
|
|
||||||
|
int frames_read = 0;
|
||||||
|
int timeout_ms = 1000;
|
||||||
|
auto start = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
while (frames_read < frames) {
|
||||||
|
size_t read_pos = capture_read_pos_.load();
|
||||||
|
size_t write_pos = capture_write_pos_.load();
|
||||||
|
size_t available = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
|
||||||
|
|
||||||
|
if (available > 0) {
|
||||||
|
int to_read = std::min((int)available, frames - frames_read);
|
||||||
|
for (int i = 0; i < to_read; i++) {
|
||||||
|
buffer[frames_read + i] = capture_buffer_[(read_pos + i) % RING_BUFFER_SIZE];
|
||||||
|
}
|
||||||
|
capture_read_pos_ = (read_pos + to_read) % RING_BUFFER_SIZE;
|
||||||
|
frames_read += to_read;
|
||||||
|
consecutive_read_failures_ = 0;
|
||||||
|
} else {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
|
||||||
|
consecutive_read_failures_++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
int write(const float* buffer, int frames) {
|
||||||
|
if (!playback_open_) return -1;
|
||||||
|
|
||||||
|
int frames_written = 0;
|
||||||
|
int timeout_ms = 1000;
|
||||||
|
auto start = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
while (frames_written < frames) {
|
||||||
|
size_t read_pos = playback_read_pos_.load();
|
||||||
|
size_t write_pos = playback_write_pos_.load();
|
||||||
|
size_t used = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
|
||||||
|
size_t available = RING_BUFFER_SIZE - 1 - used;
|
||||||
|
|
||||||
|
if (available > 0) {
|
||||||
|
int to_write = std::min((int)available, frames - frames_written);
|
||||||
|
for (int i = 0; i < to_write; i++) {
|
||||||
|
playback_buffer_[(write_pos + i) % RING_BUFFER_SIZE] = buffer[frames_written + i];
|
||||||
|
}
|
||||||
|
playback_write_pos_ = (write_pos + to_write) % RING_BUFFER_SIZE;
|
||||||
|
frames_written += to_write;
|
||||||
|
consecutive_write_failures_ = 0;
|
||||||
|
} else {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
|
||||||
|
consecutive_write_failures_++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames_written;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check audio status
|
||||||
|
bool is_healthy() const {
|
||||||
|
return capture_open_ && playback_open_ &&
|
||||||
|
consecutive_read_failures_ < 3 &&
|
||||||
|
consecutive_write_failures_ < 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to reconnect audio devices
|
||||||
|
bool reconnect() {
|
||||||
|
close_capture();
|
||||||
|
close_playback();
|
||||||
|
|
||||||
|
consecutive_read_failures_ = 0;
|
||||||
|
consecutive_write_failures_ = 0;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
if (!open_playback()) ok = false;
|
||||||
|
if (!open_capture()) ok = false;
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_silence(int frames) {
|
||||||
|
std::vector<float> silence(frames, 0.0f);
|
||||||
|
write(silence.data(), frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drain_playback() {
|
||||||
|
if (!playback_open_) return;
|
||||||
|
|
||||||
|
int timeout_ms = 2000;
|
||||||
|
auto start = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
size_t read_pos = playback_read_pos_.load();
|
||||||
|
size_t write_pos = playback_write_pos_.load();
|
||||||
|
if (read_pos == write_pos) break;
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
float measure_level(int duration_ms = 100) {
|
||||||
|
if (!capture_open_) return -100.0f;
|
||||||
|
|
||||||
|
int frames = (sample_rate_ * duration_ms) / 1000;
|
||||||
|
std::vector<float> buffer(frames);
|
||||||
|
|
||||||
|
int total_read = read(buffer.data(), frames);
|
||||||
|
if (total_read <= 0) return -100.0f;
|
||||||
|
|
||||||
|
float sum_sq = 0.0f;
|
||||||
|
for (int i = 0; i < total_read; i++) {
|
||||||
|
sum_sq += buffer[i] * buffer[i];
|
||||||
|
}
|
||||||
|
float rms = std::sqrt(sum_sq / total_read);
|
||||||
|
|
||||||
|
if (rms < 1e-10f) return -100.0f;
|
||||||
|
return 20.0f * std::log10(rms);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sample_rate() const { return sample_rate_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t RING_BUFFER_SIZE = 48000;
|
||||||
|
|
||||||
|
bool ensure_context() {
|
||||||
|
if (context_initialized_) return true;
|
||||||
|
|
||||||
|
if (ma_context_init(NULL, 0, NULL, &context_) != MA_SUCCESS) {
|
||||||
|
std::cerr << "Failed to initialize audio context" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
context_initialized_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void playback_callback(ma_device* device, void* output, const void* input, ma_uint32 frame_count) {
|
||||||
|
(void)input;
|
||||||
|
MiniAudio* self = static_cast<MiniAudio*>(device->pUserData);
|
||||||
|
float* out = static_cast<float*>(output);
|
||||||
|
|
||||||
|
size_t read_pos = self->playback_read_pos_.load();
|
||||||
|
size_t write_pos = self->playback_write_pos_.load();
|
||||||
|
size_t available = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
|
||||||
|
|
||||||
|
ma_uint32 to_read = std::min((ma_uint32)available, frame_count);
|
||||||
|
|
||||||
|
for (ma_uint32 i = 0; i < to_read; i++) {
|
||||||
|
out[i] = self->playback_buffer_[(read_pos + i) % RING_BUFFER_SIZE];
|
||||||
|
}
|
||||||
|
for (ma_uint32 i = to_read; i < frame_count; i++) {
|
||||||
|
out[i] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->playback_read_pos_ = (read_pos + to_read) % RING_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void capture_callback(ma_device* device, void* output, const void* input, ma_uint32 frame_count) {
|
||||||
|
(void)output;
|
||||||
|
MiniAudio* self = static_cast<MiniAudio*>(device->pUserData);
|
||||||
|
const float* in = static_cast<const float*>(input);
|
||||||
|
|
||||||
|
size_t read_pos = self->capture_read_pos_.load();
|
||||||
|
size_t write_pos = self->capture_write_pos_.load();
|
||||||
|
size_t used = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
|
||||||
|
size_t available = RING_BUFFER_SIZE - 1 - used;
|
||||||
|
|
||||||
|
ma_uint32 to_write = std::min((ma_uint32)available, frame_count);
|
||||||
|
|
||||||
|
for (ma_uint32 i = 0; i < to_write; i++) {
|
||||||
|
self->capture_buffer_[(write_pos + i) % RING_BUFFER_SIZE] = in[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
self->capture_write_pos_ = (write_pos + to_write) % RING_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string capture_device_id_;
|
||||||
|
std::string playback_device_id_;
|
||||||
|
int sample_rate_;
|
||||||
|
|
||||||
|
ma_context context_;
|
||||||
|
bool context_initialized_ = false;
|
||||||
|
|
||||||
|
ma_device playback_device_;
|
||||||
|
ma_device capture_device_;
|
||||||
|
ma_device_id stored_capture_id_;
|
||||||
|
ma_device_id stored_playback_id_;
|
||||||
|
bool playback_open_ = false;
|
||||||
|
bool capture_open_ = false;
|
||||||
|
|
||||||
|
std::vector<float> capture_buffer_;
|
||||||
|
std::vector<float> playback_buffer_;
|
||||||
|
std::atomic<size_t> capture_read_pos_{0};
|
||||||
|
std::atomic<size_t> capture_write_pos_{0};
|
||||||
|
std::atomic<size_t> playback_read_pos_{0};
|
||||||
|
std::atomic<size_t> playback_write_pos_{0};
|
||||||
|
|
||||||
|
int consecutive_read_failures_ = 0;
|
||||||
|
int consecutive_write_failures_ = 0;
|
||||||
|
};
|
||||||
979
modem.hh
Normal file
979
modem.hh
Normal file
|
|
@ -0,0 +1,979 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
|
||||||
|
#include "common.hh"
|
||||||
|
#include "schmidl_cox.hh"
|
||||||
|
#include "bip_buffer.hh"
|
||||||
|
#include "theil_sen.hh"
|
||||||
|
#include "blockdc.hh"
|
||||||
|
#include "hilbert.hh"
|
||||||
|
#include "phasor.hh"
|
||||||
|
#include "delay.hh"
|
||||||
|
#include "polar_encoder.hh"
|
||||||
|
#include "polar_list_decoder.hh"
|
||||||
|
#include "hadamard_decoder.hh"
|
||||||
|
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
class BufferWritePCM {
|
||||||
|
public:
|
||||||
|
BufferWritePCM(int rate, int bits, int channels)
|
||||||
|
: rate_(rate), bits_(bits), channels_(channels) {}
|
||||||
|
|
||||||
|
void write(const T* buffer, int count, int ch = 1) {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
// 2 channels, only take real part for mono output
|
||||||
|
if (ch == 2) {
|
||||||
|
samples_.push_back(buffer[i * 2]); // real
|
||||||
|
} else {
|
||||||
|
samples_.push_back(buffer[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void silence(int count) {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
samples_.push_back(T(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<T>& samples() const { return samples_; }
|
||||||
|
std::vector<T>& samples() { return samples_; }
|
||||||
|
void clear() { samples_.clear(); }
|
||||||
|
|
||||||
|
int rate() const { return rate_; }
|
||||||
|
int bits() const { return bits_; }
|
||||||
|
int channels() const { return channels_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<T> samples_;
|
||||||
|
int rate_, bits_, channels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modem configuration
|
||||||
|
struct ModemConfig {
|
||||||
|
int sample_rate = 48000;
|
||||||
|
int center_freq = 1500;
|
||||||
|
int64_t call_sign = 0;
|
||||||
|
int oper_mode = 0;
|
||||||
|
|
||||||
|
static int64_t encode_callsign(const char* str) {
|
||||||
|
int64_t acc = 0;
|
||||||
|
for (char c = *str++; c; c = *str++) {
|
||||||
|
acc *= 40;
|
||||||
|
if (c == '/')
|
||||||
|
acc += 3;
|
||||||
|
else if (c >= '0' && c <= '9')
|
||||||
|
acc += c - '0' + 4;
|
||||||
|
else if (c >= 'a' && c <= 'z')
|
||||||
|
acc += c - 'a' + 14;
|
||||||
|
else if (c >= 'A' && c <= 'Z')
|
||||||
|
acc += c - 'A' + 14;
|
||||||
|
else if (c != ' ')
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int encode_mode(const char* modulation, const char* code_rate, bool short_frame) {
|
||||||
|
int mode = 0;
|
||||||
|
|
||||||
|
if (!strcmp(modulation, "BPSK"))
|
||||||
|
mode |= 0 << 4;
|
||||||
|
else if (!strcmp(modulation, "QPSK"))
|
||||||
|
mode |= 1 << 4;
|
||||||
|
else if (!strcmp(modulation, "8PSK"))
|
||||||
|
mode |= 2 << 4;
|
||||||
|
else if (!strcmp(modulation, "QAM16"))
|
||||||
|
mode |= 3 << 4;
|
||||||
|
else if (!strcmp(modulation, "QAM64"))
|
||||||
|
mode |= 4 << 4;
|
||||||
|
else if (!strcmp(modulation, "QAM256"))
|
||||||
|
mode |= 5 << 4;
|
||||||
|
else if (!strcmp(modulation, "QAM1024"))
|
||||||
|
mode |= 6 << 4;
|
||||||
|
else if (!strcmp(modulation, "QAM4096"))
|
||||||
|
mode |= 7 << 4;
|
||||||
|
else
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (!strcmp(code_rate, "1/2"))
|
||||||
|
mode |= 0 << 1;
|
||||||
|
else if (!strcmp(code_rate, "2/3"))
|
||||||
|
mode |= 1 << 1;
|
||||||
|
else if (!strcmp(code_rate, "3/4"))
|
||||||
|
mode |= 2 << 1;
|
||||||
|
else if (!strcmp(code_rate, "5/6"))
|
||||||
|
mode |= 3 << 1;
|
||||||
|
else
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (!short_frame)
|
||||||
|
mode |= 1;
|
||||||
|
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encoder
|
||||||
|
template<typename value, typename cmplx, int rate>
|
||||||
|
class ModemEncoder : public Common {
|
||||||
|
public:
|
||||||
|
typedef int8_t code_type;
|
||||||
|
static const int guard_len = rate / 300;
|
||||||
|
static const int symbol_len = guard_len * 40;
|
||||||
|
|
||||||
|
ModemEncoder() {}
|
||||||
|
|
||||||
|
// encode our data to audio samples
|
||||||
|
std::vector<value> encode(const uint8_t* input_data, size_t input_len,
|
||||||
|
int freq_off, int64_t call_sign, int oper_mode) {
|
||||||
|
BufferWritePCM<value> pcm(rate, 32, 1);
|
||||||
|
|
||||||
|
if (!setup(oper_mode)) {
|
||||||
|
std::cerr << "Encoder: invalid mode" << std::endl;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = (freq_off * symbol_len) / rate;
|
||||||
|
tone_off = offset - tone_count / 2;
|
||||||
|
|
||||||
|
guard_interval_weights();
|
||||||
|
meta_data((call_sign << 8) | oper_mode);
|
||||||
|
|
||||||
|
// leading noise
|
||||||
|
CODE::MLS noise(mls2_poly);
|
||||||
|
for (int j = 0; j < 1; ++j) {
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
tone[i] = nrz(noise());
|
||||||
|
symbol(&pcm, -3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy input data (pad if necessary)
|
||||||
|
std::memset(data, 0, data_max);
|
||||||
|
std::memcpy(data, input_data, std::min(input_len, (size_t)data_bytes));
|
||||||
|
|
||||||
|
// Scramble
|
||||||
|
CODE::Xorshift32 scrambler;
|
||||||
|
for (int i = 0; i < data_bytes; ++i)
|
||||||
|
data[i] ^= scrambler();
|
||||||
|
|
||||||
|
// Schmidl-Cox preamble
|
||||||
|
CODE::MLS seq0(mls0_poly, mls0_seed);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
tone[i] = nrz(seq0());
|
||||||
|
symbol(&pcm, -2);
|
||||||
|
symbol(&pcm, -1);
|
||||||
|
|
||||||
|
// Encode payload
|
||||||
|
for (int i = 0; i < data_bits; ++i)
|
||||||
|
mesg[i] = nrz(CODE::get_le_bit(data, i));
|
||||||
|
|
||||||
|
crc1.reset();
|
||||||
|
for (int i = 0; i < data_bytes; ++i)
|
||||||
|
crc1(data[i]);
|
||||||
|
for (int i = 0; i < 32; ++i)
|
||||||
|
mesg[i + data_bits] = nrz((crc1() >> i) & 1);
|
||||||
|
|
||||||
|
polar_encoder(code, mesg, frozen_bits, code_order);
|
||||||
|
shuffle(perm, code, code_order);
|
||||||
|
|
||||||
|
// Generate symbols
|
||||||
|
CODE::MLS seq1(mls1_poly);
|
||||||
|
for (int j = 0, k = 0, m = 0; j < symbol_count + 1; ++j) {
|
||||||
|
seed_off = (block_skew * j + first_seed) % block_length;
|
||||||
|
for (int i = 0; i < tone_count; ++i) {
|
||||||
|
if (i % block_length == seed_off) {
|
||||||
|
tone[i] = nrz(seq1());
|
||||||
|
} else if (j) {
|
||||||
|
int bits = mod_bits;
|
||||||
|
if (mod_bits == 3 && k % 32 == 30) bits = 2;
|
||||||
|
if (mod_bits == 6 && k % 64 == 60) bits = 4;
|
||||||
|
if (mod_bits == 10 && k % 128 == 120) bits = 8;
|
||||||
|
if (mod_bits == 12 && k % 128 == 120) bits = 8;
|
||||||
|
tone[i] = map_bits(perm + k, bits);
|
||||||
|
k += bits;
|
||||||
|
} else {
|
||||||
|
tone[i] = map_bits(meta + m++, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
symbol(&pcm, j);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < guard_len; ++i)
|
||||||
|
guard[i] *= 1 - weight[i];
|
||||||
|
pcm.write(reinterpret_cast<value*>(guard), guard_len, 2);
|
||||||
|
|
||||||
|
|
||||||
|
return std::move(pcm.samples());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_payload_size(int oper_mode) {
|
||||||
|
if (!setup(oper_mode)) return 0;
|
||||||
|
return data_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
DSP::FastFourierTransform<symbol_len, cmplx, -1> fwd;
|
||||||
|
DSP::FastFourierTransform<symbol_len, cmplx, 1> bwd;
|
||||||
|
CODE::PolarEncoder<code_type> polar_encoder;
|
||||||
|
code_type code[bits_max], perm[bits_max], mesg[bits_max], meta[data_tones];
|
||||||
|
cmplx fdom[symbol_len];
|
||||||
|
cmplx tdom[symbol_len];
|
||||||
|
cmplx test[symbol_len];
|
||||||
|
cmplx kern[symbol_len];
|
||||||
|
cmplx guard[guard_len];
|
||||||
|
cmplx tone[tone_count];
|
||||||
|
cmplx temp[tone_count];
|
||||||
|
value weight[guard_len];
|
||||||
|
value papr[symbols_max];
|
||||||
|
|
||||||
|
static int bin(int carrier) {
|
||||||
|
return (carrier + symbol_len) % symbol_len;
|
||||||
|
}
|
||||||
|
static int nrz(bool bit) {
|
||||||
|
return 1 - 2 * bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmplx map_bits(code_type* b, int bits) {
|
||||||
|
switch (bits) {
|
||||||
|
case 1: return PhaseShiftKeying<2, cmplx, code_type>::map(b);
|
||||||
|
case 2: return PhaseShiftKeying<4, cmplx, code_type>::map(b);
|
||||||
|
case 3: return PhaseShiftKeying<8, cmplx, code_type>::map(b);
|
||||||
|
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::map(b);
|
||||||
|
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::map(b);
|
||||||
|
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::map(b);
|
||||||
|
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::map(b);
|
||||||
|
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::map(b);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shuffle(code_type* dest, const code_type* src, int order) {
|
||||||
|
if (order == 8) {
|
||||||
|
CODE::XorShiftMask<int, 8, 1, 1, 2, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 256; ++i) dest[i] = src[seq()];
|
||||||
|
} else if (order == 11) {
|
||||||
|
CODE::XorShiftMask<int, 11, 1, 3, 4, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 2048; ++i) dest[i] = src[seq()];
|
||||||
|
} else if (order == 12) {
|
||||||
|
CODE::XorShiftMask<int, 12, 1, 1, 4, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 4096; ++i) dest[i] = src[seq()];
|
||||||
|
} else if (order == 13) {
|
||||||
|
CODE::XorShiftMask<int, 13, 1, 1, 9, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 8192; ++i) dest[i] = src[seq()];
|
||||||
|
} else if (order == 14) {
|
||||||
|
CODE::XorShiftMask<int, 14, 1, 5, 10, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 16384; ++i) dest[i] = src[seq()];
|
||||||
|
} else if (order == 15) {
|
||||||
|
CODE::XorShiftMask<int, 15, 1, 1, 3, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 32768; ++i) dest[i] = src[seq()];
|
||||||
|
} else if (order == 16) {
|
||||||
|
CODE::XorShiftMask<int, 16, 1, 1, 14, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 65536; ++i) dest[i] = src[seq()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void guard_interval_weights() {
|
||||||
|
for (int i = 0; i < guard_len / 4; ++i)
|
||||||
|
weight[i] = 0;
|
||||||
|
for (int i = guard_len / 4; i < guard_len / 4 + guard_len / 2; ++i) {
|
||||||
|
value x = value(i - guard_len / 4) / value(guard_len / 2 - 1);
|
||||||
|
weight[i] = value(0.5) * (value(1) - std::cos(DSP::Const<value>::Pi() * x));
|
||||||
|
}
|
||||||
|
for (int i = guard_len / 4 + guard_len / 2; i < guard_len; ++i)
|
||||||
|
weight[i] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clipping_and_filtering(value scale) {
|
||||||
|
for (int i = 0; i < symbol_len; ++i) {
|
||||||
|
value pwr = norm(tdom[i]);
|
||||||
|
if (pwr > value(1))
|
||||||
|
tdom[i] /= sqrt(pwr);
|
||||||
|
}
|
||||||
|
fwd(fdom, tdom);
|
||||||
|
for (int i = 0; i < symbol_len; ++i) {
|
||||||
|
int j = bin(i + tone_off);
|
||||||
|
if (i >= tone_count)
|
||||||
|
fdom[j] = 0;
|
||||||
|
else
|
||||||
|
fdom[j] *= 1 / (scale * symbol_len);
|
||||||
|
}
|
||||||
|
bwd(tdom, fdom);
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] *= scale;
|
||||||
|
auto clamp = [](value v) { return v < value(-1) ? value(-1) : v > value(1) ? value(1) : v; };
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] = cmplx(clamp(tdom[i].real()), clamp(tdom[i].imag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void symbol(BufferWritePCM<value>* pcm, int symbol_number) {
|
||||||
|
value scale = value(0.5) / std::sqrt(value(tone_count));
|
||||||
|
if (symbol_number < 0) {
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
fdom[i] = 0;
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
fdom[bin(i + tone_off)] = tone[i];
|
||||||
|
bwd(tdom, fdom);
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] *= scale;
|
||||||
|
} else {
|
||||||
|
value best_papr = 1000;
|
||||||
|
for (int seed_value = 0; seed_value < 128; ++seed_value) {
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
temp[i] = tone[i];
|
||||||
|
hadamard_encoder(seed, seed_value);
|
||||||
|
for (int i = 0; i < seed_tones; ++i)
|
||||||
|
temp[i * block_length + seed_off] *= seed[i];
|
||||||
|
if (seed_value) {
|
||||||
|
CODE::MLS seq(mls2_poly, seed_value);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
if (i % block_length != seed_off)
|
||||||
|
temp[i] *= nrz(seq());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
fdom[i] = 0;
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
fdom[bin(i + tone_off)] = temp[i];
|
||||||
|
bwd(test, fdom);
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
test[i] *= scale;
|
||||||
|
value peak = 0, mean = 0;
|
||||||
|
for (int i = 0; i < symbol_len; ++i) {
|
||||||
|
value power(norm(test[i]));
|
||||||
|
peak = std::max(peak, power);
|
||||||
|
mean += power;
|
||||||
|
}
|
||||||
|
mean /= symbol_len;
|
||||||
|
value test_papr(peak / mean);
|
||||||
|
if (test_papr < best_papr) {
|
||||||
|
best_papr = test_papr;
|
||||||
|
papr[symbol_number] = test_papr;
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] = test[i];
|
||||||
|
if (test_papr < 5)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clipping_and_filtering(scale);
|
||||||
|
if (symbol_number != -1) {
|
||||||
|
for (int i = 0; i < guard_len; ++i)
|
||||||
|
guard[i] = DSP::lerp(guard[i], tdom[i + symbol_len - guard_len], weight[i]);
|
||||||
|
pcm->write(reinterpret_cast<value*>(guard), guard_len, 2);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < guard_len; ++i)
|
||||||
|
guard[i] = tdom[i];
|
||||||
|
pcm->write(reinterpret_cast<value*>(tdom), symbol_len, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void meta_data(uint64_t md) {
|
||||||
|
for (int i = 0; i < 56; ++i)
|
||||||
|
mesg[i] = nrz((md >> i) & 1);
|
||||||
|
crc0.reset();
|
||||||
|
crc0(md << 8);
|
||||||
|
for (int i = 0; i < 16; ++i)
|
||||||
|
mesg[i + 56] = nrz((crc0() >> i) & 1);
|
||||||
|
polar_encoder(code, mesg, frozen_256_72, 8);
|
||||||
|
shuffle(meta, code, 8);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decoder
|
||||||
|
template<typename value, typename cmplx, int rate>
|
||||||
|
class ModemDecoder : public Common {
|
||||||
|
public:
|
||||||
|
typedef int16_t code_type;
|
||||||
|
typedef SIMD<code_type, 32> mesg_type;
|
||||||
|
typedef DSP::Const<value> Const;
|
||||||
|
static const int guard_len = rate / 300;
|
||||||
|
static const int symbol_len = guard_len * 40;
|
||||||
|
static const int filter_len = 129;
|
||||||
|
static const int extended_len = symbol_len + guard_len;
|
||||||
|
static const int buffer_len = 5 * extended_len;
|
||||||
|
static const int search_pos = extended_len;
|
||||||
|
static const int tone_off_const = -tone_count / 2;
|
||||||
|
|
||||||
|
using FrameCallback = std::function<void(const uint8_t*, size_t)>;
|
||||||
|
|
||||||
|
ModemDecoder() {
|
||||||
|
// init fdom_mls before correlator uses it
|
||||||
|
init_mls0_seq();
|
||||||
|
correlator_ptr = new SchmidlCox<value, cmplx, search_pos, symbol_len, guard_len>(fdom_mls);
|
||||||
|
blockdc.samples(filter_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ModemDecoder() {
|
||||||
|
delete correlator_ptr;
|
||||||
|
delete seq1_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void process(const value* samples, size_t count, FrameCallback callback) {
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
process_sample(samples[i], callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset decoder state
|
||||||
|
void reset() {
|
||||||
|
state_ = State::SEARCHING;
|
||||||
|
sample_count_ = 0;
|
||||||
|
symbol_index_ = 0;
|
||||||
|
samples_needed_ = 0;
|
||||||
|
k_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get average SNR from last successful decode
|
||||||
|
value get_last_snr() const { return last_avg_snr_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class State {
|
||||||
|
SEARCHING, // looking for preamble
|
||||||
|
COLLECTING_SYMBOLS, // Collecting data symbols
|
||||||
|
};
|
||||||
|
|
||||||
|
// Arrays used by correlator
|
||||||
|
cmplx fdom_mls[symbol_len];
|
||||||
|
cmplx fdom[symbol_len], tdom[symbol_len];
|
||||||
|
|
||||||
|
DSP::FastFourierTransform<symbol_len, cmplx, -1> fwd;
|
||||||
|
DSP::BlockDC<value, value> blockdc;
|
||||||
|
DSP::Hilbert<cmplx, filter_len> hilbert;
|
||||||
|
DSP::BipBuffer<cmplx, buffer_len> input_hist;
|
||||||
|
DSP::TheilSenEstimator<value, tone_count> tse;
|
||||||
|
SchmidlCox<value, cmplx, search_pos, symbol_len, guard_len>* correlator_ptr = nullptr;
|
||||||
|
CODE::HadamardDecoder<7> hadamard_decoder;
|
||||||
|
CODE::PolarListDecoder<mesg_type, code_max> polar_decoder;
|
||||||
|
DSP::Phasor<cmplx> osc;
|
||||||
|
|
||||||
|
mesg_type mesg[bits_max];
|
||||||
|
code_type code[bits_max], perm[bits_max];
|
||||||
|
cmplx demod[tone_count], chan[tone_count], tone[tone_count];
|
||||||
|
value index[tone_count], phase[tone_count];
|
||||||
|
value snr[symbols_max];
|
||||||
|
value cfo_rad;
|
||||||
|
int symbol_pos;
|
||||||
|
value last_avg_snr_ = 0;
|
||||||
|
|
||||||
|
State state_ = State::SEARCHING;
|
||||||
|
size_t sample_count_ = 0;
|
||||||
|
int symbol_index_ = 0;
|
||||||
|
int samples_needed_ = 0;
|
||||||
|
int k_ = 0;
|
||||||
|
const cmplx* buf_ = nullptr;
|
||||||
|
CODE::MLS* seq1_ptr = nullptr;
|
||||||
|
|
||||||
|
static int bin(int carrier) {
|
||||||
|
return (carrier + symbol_len) % symbol_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static value nrz(bool bit) {
|
||||||
|
return 1 - 2 * bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cmplx demod_or_erase(cmplx curr, cmplx prev) {
|
||||||
|
if (norm(prev) > 0) {
|
||||||
|
cmplx d = curr / prev;
|
||||||
|
if (norm(d) < 4)
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_mls0_seq() {
|
||||||
|
CODE::MLS seq0(mls0_poly, mls0_seed);
|
||||||
|
value cur = 0, prv = 0;
|
||||||
|
for (int i = 0; i < tone_count; ++i, prv = cur)
|
||||||
|
fdom_mls[bin(i + tone_off_const)] = prv * (cur = nrz(seq0()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cmplx map_bits(code_type* b, int bits) {
|
||||||
|
switch (bits) {
|
||||||
|
case 1: return PhaseShiftKeying<2, cmplx, code_type>::map(b);
|
||||||
|
case 2: return PhaseShiftKeying<4, cmplx, code_type>::map(b);
|
||||||
|
case 3: return PhaseShiftKeying<8, cmplx, code_type>::map(b);
|
||||||
|
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::map(b);
|
||||||
|
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::map(b);
|
||||||
|
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::map(b);
|
||||||
|
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::map(b);
|
||||||
|
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::map(b);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void demap_soft(code_type* b, cmplx c, value precision, int bits) {
|
||||||
|
switch (bits) {
|
||||||
|
case 1: return PhaseShiftKeying<2, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 2: return PhaseShiftKeying<4, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 3: return PhaseShiftKeying<8, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::soft(b, c, precision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void demap_hard(code_type* b, cmplx c, int bits) {
|
||||||
|
switch (bits) {
|
||||||
|
case 1: return PhaseShiftKeying<2, cmplx, code_type>::hard(b, c);
|
||||||
|
case 2: return PhaseShiftKeying<4, cmplx, code_type>::hard(b, c);
|
||||||
|
case 3: return PhaseShiftKeying<8, cmplx, code_type>::hard(b, c);
|
||||||
|
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::hard(b, c);
|
||||||
|
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::hard(b, c);
|
||||||
|
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::hard(b, c);
|
||||||
|
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::hard(b, c);
|
||||||
|
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::hard(b, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void shuffle(code_type* dest, const code_type* src, int order) {
|
||||||
|
if (order == 8) {
|
||||||
|
CODE::XorShiftMask<int, 8, 1, 1, 2, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 256; ++i) dest[seq()] = src[i];
|
||||||
|
} else if (order == 11) {
|
||||||
|
CODE::XorShiftMask<int, 11, 1, 3, 4, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 2048; ++i) dest[seq()] = src[i];
|
||||||
|
} else if (order == 12) {
|
||||||
|
CODE::XorShiftMask<int, 12, 1, 1, 4, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 4096; ++i) dest[seq()] = src[i];
|
||||||
|
} else if (order == 13) {
|
||||||
|
CODE::XorShiftMask<int, 13, 1, 1, 9, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 8192; ++i) dest[seq()] = src[i];
|
||||||
|
} else if (order == 14) {
|
||||||
|
CODE::XorShiftMask<int, 14, 1, 5, 10, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 16384; ++i) dest[seq()] = src[i];
|
||||||
|
} else if (order == 15) {
|
||||||
|
CODE::XorShiftMask<int, 15, 1, 1, 3, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 32768; ++i) dest[seq()] = src[i];
|
||||||
|
} else if (order == 16) {
|
||||||
|
CODE::XorShiftMask<int, 16, 1, 1, 14, 1> seq;
|
||||||
|
dest[0] = src[0];
|
||||||
|
for (int i = 1; i < 65536; ++i) dest[seq()] = src[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void base40_decoder(char* str, int64_t val, int len) {
|
||||||
|
for (int i = len - 1; i >= 0; --i, val /= 40)
|
||||||
|
str[i] = " /0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[val % 40];
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t meta_data() {
|
||||||
|
shuffle(code, perm, 8);
|
||||||
|
polar_decoder(nullptr, mesg, code, frozen_256_72, 8);
|
||||||
|
int best = -1;
|
||||||
|
for (int k = 0; k < mesg_type::SIZE; ++k) {
|
||||||
|
crc0.reset();
|
||||||
|
for (int i = 0; i < 72; ++i)
|
||||||
|
crc0(mesg[i].v[k] < 0);
|
||||||
|
if (crc0() == 0) {
|
||||||
|
best = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best < 0)
|
||||||
|
return -1;
|
||||||
|
uint64_t md = 0;
|
||||||
|
for (int i = 0; i < 56; ++i)
|
||||||
|
md |= uint64_t(mesg[i].v[best] < 0) << i;
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
|
||||||
|
void process_sample(value sample, FrameCallback callback) {
|
||||||
|
// Convert to complex via Hilbert transform
|
||||||
|
cmplx tmp = hilbert(blockdc(sample));
|
||||||
|
buf_ = input_hist(tmp);
|
||||||
|
++sample_count_;
|
||||||
|
|
||||||
|
switch (state_) {
|
||||||
|
case State::SEARCHING:
|
||||||
|
if ((*correlator_ptr)(buf_)) {
|
||||||
|
// Sync found
|
||||||
|
symbol_pos = correlator_ptr->symbol_pos;
|
||||||
|
cfo_rad = correlator_ptr->cfo_rad;
|
||||||
|
|
||||||
|
std::cerr << "Decoder: Sync found at sample " << sample_count_ << std::endl;
|
||||||
|
std::cerr << "Decoder: CFO = " << cfo_rad * (rate / Const::TwoPi()) << " Hz" << std::endl;
|
||||||
|
|
||||||
|
// Initialize seq1 for the whole frame
|
||||||
|
delete seq1_ptr;
|
||||||
|
seq1_ptr = new CODE::MLS(mls1_poly);
|
||||||
|
|
||||||
|
// Process preamble and start collecting symbols
|
||||||
|
if (process_preamble()) {
|
||||||
|
state_ = State::COLLECTING_SYMBOLS;
|
||||||
|
symbol_index_ = 1; // Symbol 0 (meta) already processed
|
||||||
|
// Need to advance past preamble: symbol_pos + symbol_len + extended_len
|
||||||
|
// Plus extended_len for the first data symbol
|
||||||
|
samples_needed_ = symbol_pos + symbol_len + 2 * extended_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::COLLECTING_SYMBOLS:
|
||||||
|
// Keep feeding correlator to maintain buffer
|
||||||
|
(*correlator_ptr)(buf_);
|
||||||
|
samples_needed_--;
|
||||||
|
|
||||||
|
if (samples_needed_ <= 0) {
|
||||||
|
// Process this symbol
|
||||||
|
if (!process_symbol(symbol_index_)) {
|
||||||
|
// Error, go back to searching
|
||||||
|
state_ = State::SEARCHING;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol_index_++;
|
||||||
|
|
||||||
|
if (symbol_index_ > symbol_count) {
|
||||||
|
// All symbols collected
|
||||||
|
decode_frame(callback);
|
||||||
|
state_ = State::SEARCHING;
|
||||||
|
} else {
|
||||||
|
samples_needed_ = extended_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process_preamble() {
|
||||||
|
// Process Schmidl-Cox preamble symbols
|
||||||
|
osc.omega(-cfo_rad);
|
||||||
|
|
||||||
|
// First preamble symbol
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] = buf_[i + symbol_pos] * osc();
|
||||||
|
fwd(fdom, tdom);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
tone[i] = fdom[bin(i + tone_off_const)];
|
||||||
|
|
||||||
|
// Second preamble symbol
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] = buf_[i + symbol_pos + symbol_len] * osc();
|
||||||
|
for (int i = 0; i < guard_len; ++i)
|
||||||
|
osc();
|
||||||
|
fwd(fdom, tdom);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
chan[i] = fdom[bin(i + tone_off_const)];
|
||||||
|
|
||||||
|
// Estimate SFO
|
||||||
|
for (int i = 0; i < tone_count; ++i) {
|
||||||
|
index[i] = tone_off_const + i;
|
||||||
|
phase[i] = arg(demod_or_erase(chan[i], tone[i]));
|
||||||
|
}
|
||||||
|
tse.compute(index, phase, tone_count);
|
||||||
|
|
||||||
|
std::cerr << "Decoder: SFO = " << -1000000 * tse.slope() / Const::TwoPi() << " ppm" << std::endl;
|
||||||
|
|
||||||
|
// Correct channel estimate
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
tone[i] *= DSP::polar<value>(1, tse(i + tone_off_const));
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
chan[i] = DSP::lerp(chan[i], tone[i], value(0.5));
|
||||||
|
|
||||||
|
// Remove preamble sequence
|
||||||
|
CODE::MLS seq0(mls0_poly, mls0_seed);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
chan[i] *= nrz(seq0());
|
||||||
|
|
||||||
|
// Process meta symbol (symbol 0)
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] = buf_[i + symbol_pos + symbol_len + extended_len] * osc();
|
||||||
|
for (int i = 0; i < guard_len; ++i)
|
||||||
|
osc();
|
||||||
|
fwd(fdom, tdom);
|
||||||
|
|
||||||
|
// Decode meta symbol
|
||||||
|
seed_off = first_seed;
|
||||||
|
auto clamp = [](int v) { return v < -127 ? -127 : v > 127 ? 127 : v; };
|
||||||
|
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
tone[i] = fdom[bin(i + tone_off_const)];
|
||||||
|
for (int i = seed_off; i < tone_count; i += block_length)
|
||||||
|
tone[i] *= nrz((*seq1_ptr)());
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
demod[i] = demod_or_erase(tone[i], chan[i]);
|
||||||
|
|
||||||
|
// Decode seed for meta symbol
|
||||||
|
for (int i = 0; i < seed_tones; ++i)
|
||||||
|
seed[i] = clamp(std::nearbyint(127 * demod[i * block_length + seed_off].real()));
|
||||||
|
int seed_value = hadamard_decoder(seed);
|
||||||
|
if (seed_value < 0) {
|
||||||
|
std::cerr << "Decoder: Seed value damaged in meta" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hadamard_encoder(seed, seed_value);
|
||||||
|
for (int i = 0; i < seed_tones; ++i) {
|
||||||
|
tone[block_length * i + seed_off] *= seed[i];
|
||||||
|
demod[block_length * i + seed_off] *= seed[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase correction
|
||||||
|
for (int i = 0; i < seed_tones; ++i) {
|
||||||
|
index[i] = tone_off_const + block_length * i + seed_off;
|
||||||
|
phase[i] = arg(demod[block_length * i + seed_off]);
|
||||||
|
}
|
||||||
|
tse.compute(index, phase, seed_tones);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
demod[i] *= DSP::polar<value>(1, -tse(i + tone_off_const));
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
chan[i] *= DSP::polar<value>(1, tse(i + tone_off_const));
|
||||||
|
|
||||||
|
if (seed_value) {
|
||||||
|
CODE::MLS seq(mls2_poly, seed_value);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
if (i % block_length != seed_off)
|
||||||
|
demod[i] *= nrz(seq());
|
||||||
|
}
|
||||||
|
|
||||||
|
// SNR estimation and demapping for meta symbol (mod_bits = 1 for meta)
|
||||||
|
value sp = 0, np = 0;
|
||||||
|
for (int i = 0, l = 0; i < tone_count; ++i) {
|
||||||
|
cmplx hard(1, 0);
|
||||||
|
if (i % block_length != seed_off) {
|
||||||
|
demap_hard(perm + l, demod[i], 1);
|
||||||
|
hard = map_bits(perm + l, 1);
|
||||||
|
l += 1;
|
||||||
|
}
|
||||||
|
cmplx error = demod[i] - hard;
|
||||||
|
sp += norm(hard);
|
||||||
|
np += norm(error);
|
||||||
|
}
|
||||||
|
value precision = sp / np;
|
||||||
|
precision = std::min(precision, value(1023));
|
||||||
|
|
||||||
|
// std::cerr << "Decoder: Meta symbol SNR = " << 10 * std::log10(precision) << " dB" << std::endl;
|
||||||
|
|
||||||
|
// Soft demap meta symbol
|
||||||
|
int k = 0;
|
||||||
|
for (int i = 0; i < tone_count; ++i) {
|
||||||
|
if (i % block_length != seed_off) {
|
||||||
|
demap_soft(perm + k, demod[i], precision, 1);
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update channel for meta symbol pilots
|
||||||
|
for (int i = seed_off; i < tone_count; i += block_length)
|
||||||
|
chan[i] = DSP::lerp(chan[i], tone[i], value(0.5));
|
||||||
|
|
||||||
|
// Decode meta data
|
||||||
|
int64_t meta_info = meta_data();
|
||||||
|
if (meta_info < 0) {
|
||||||
|
std::cerr << "Decoder: Preamble decoding error" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t call = meta_info >> 8;
|
||||||
|
if (call == 0 || call >= 262144000000000L) {
|
||||||
|
std::cerr << "Decoder: Invalid call sign" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char call_sign[10];
|
||||||
|
base40_decoder(call_sign, call, 9);
|
||||||
|
call_sign[9] = 0;
|
||||||
|
std::cerr << "Decoder: Call sign: " << call_sign << std::endl;
|
||||||
|
|
||||||
|
int mode = meta_info & 255;
|
||||||
|
if (!setup(mode)) {
|
||||||
|
std::cerr << "Decoder: Invalid mode" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "Decoder: Mode " << oper_mode << ", " << symbol_count << " data symbols, mod_bits=" << mod_bits << ", code_order=" << code_order << ", data_bytes=" << data_bytes << std::endl;
|
||||||
|
|
||||||
|
// Reset for data collection
|
||||||
|
k_ = 0;
|
||||||
|
snr[0] = 100;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process_symbol(int j) {
|
||||||
|
seed_off = (block_skew * j + first_seed) % block_length;
|
||||||
|
auto clamp = [](int v) { return v < -127 ? -127 : v > 127 ? 127 : v; };
|
||||||
|
|
||||||
|
// FFT the current symbol
|
||||||
|
for (int i = 0; i < symbol_len; ++i)
|
||||||
|
tdom[i] = buf_[i] * osc();
|
||||||
|
for (int i = 0; i < guard_len; ++i)
|
||||||
|
osc();
|
||||||
|
fwd(fdom, tdom);
|
||||||
|
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
tone[i] = fdom[bin(i + tone_off_const)];
|
||||||
|
|
||||||
|
// Remove pilot sequence
|
||||||
|
for (int i = seed_off; i < tone_count; i += block_length)
|
||||||
|
tone[i] *= nrz((*seq1_ptr)());
|
||||||
|
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
demod[i] = demod_or_erase(tone[i], chan[i]);
|
||||||
|
|
||||||
|
// Decode seed
|
||||||
|
for (int i = 0; i < seed_tones; ++i)
|
||||||
|
seed[i] = clamp(std::nearbyint(127 * demod[i * block_length + seed_off].real()));
|
||||||
|
int seed_value = hadamard_decoder(seed);
|
||||||
|
if (seed_value < 0) {
|
||||||
|
std::cerr << "Decoder: Seed damaged at symbol " << j << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hadamard_encoder(seed, seed_value);
|
||||||
|
for (int i = 0; i < seed_tones; ++i) {
|
||||||
|
tone[block_length * i + seed_off] *= seed[i];
|
||||||
|
demod[block_length * i + seed_off] *= seed[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase correction
|
||||||
|
for (int i = 0; i < seed_tones; ++i) {
|
||||||
|
index[i] = tone_off_const + block_length * i + seed_off;
|
||||||
|
phase[i] = arg(demod[block_length * i + seed_off]);
|
||||||
|
}
|
||||||
|
tse.compute(index, phase, seed_tones);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
demod[i] *= DSP::polar<value>(1, -tse(i + tone_off_const));
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
chan[i] *= DSP::polar<value>(1, tse(i + tone_off_const));
|
||||||
|
|
||||||
|
if (seed_value) {
|
||||||
|
CODE::MLS seq(mls2_poly, seed_value);
|
||||||
|
for (int i = 0; i < tone_count; ++i)
|
||||||
|
if (i % block_length != seed_off)
|
||||||
|
demod[i] *= nrz(seq());
|
||||||
|
}
|
||||||
|
|
||||||
|
// SNR estimation and soft demapping
|
||||||
|
value sp = 0, np = 0;
|
||||||
|
for (int i = 0, l = k_; i < tone_count; ++i) {
|
||||||
|
cmplx hard(1, 0);
|
||||||
|
if (i % block_length != seed_off) {
|
||||||
|
int bits = mod_bits;
|
||||||
|
if (mod_bits == 3 && l % 32 == 30) bits = 2;
|
||||||
|
if (mod_bits == 6 && l % 64 == 60) bits = 4;
|
||||||
|
if (mod_bits == 10 && l % 128 == 120) bits = 8;
|
||||||
|
if (mod_bits == 12 && l % 128 == 120) bits = 8;
|
||||||
|
demap_hard(perm + l, demod[i], bits);
|
||||||
|
hard = map_bits(perm + l, bits);
|
||||||
|
l += bits;
|
||||||
|
}
|
||||||
|
cmplx error = demod[i] - hard;
|
||||||
|
sp += norm(hard);
|
||||||
|
np += norm(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
value precision = sp / np;
|
||||||
|
snr[j] = precision;
|
||||||
|
precision = std::min(precision, value(1023));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
std::cerr << "Decoder: Symbol " << j << " SNR = " << 10 * std::log10(snr[j]) << " dB, k=" << k_ << std::endl;
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < tone_count; ++i) {
|
||||||
|
if (i % block_length != seed_off) {
|
||||||
|
int bits = mod_bits;
|
||||||
|
if (mod_bits == 3 && k_ % 32 == 30) bits = 2;
|
||||||
|
if (mod_bits == 6 && k_ % 64 == 60) bits = 4;
|
||||||
|
if (mod_bits == 10 && k_ % 128 == 120) bits = 8;
|
||||||
|
if (mod_bits == 12 && k_ % 128 == 120) bits = 8;
|
||||||
|
demap_soft(perm + k_, demod[i], precision, bits);
|
||||||
|
k_ += bits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = seed_off; i < tone_count; i += block_length)
|
||||||
|
chan[i] = DSP::lerp(chan[i], tone[i], value(0.5));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void decode_frame(FrameCallback callback) {
|
||||||
|
std::cerr << "Decoder: Decoding frame, k_=" << k_ << " bits collected" << std::endl;
|
||||||
|
std::cerr << "Decoder: Expected code_order=" << code_order << " (code length=" << (1 << code_order) << ")" << std::endl;
|
||||||
|
|
||||||
|
int crc_bits = data_bits + 32;
|
||||||
|
shuffle(code, perm, code_order);
|
||||||
|
polar_decoder(nullptr, mesg, code, frozen_bits, code_order);
|
||||||
|
|
||||||
|
int best = -1;
|
||||||
|
for (int k = 0; k < mesg_type::SIZE; ++k) {
|
||||||
|
crc1.reset();
|
||||||
|
for (int i = 0; i < crc_bits; ++i)
|
||||||
|
crc1(mesg[i].v[k] < 0);
|
||||||
|
if (crc1() == 0) {
|
||||||
|
best = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best < 0) {
|
||||||
|
std::cerr << "Decoder: CRC failed" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate average SNR from data symbols
|
||||||
|
value total_snr = 0;
|
||||||
|
int snr_count = 0;
|
||||||
|
for (int i = 1; i < symbol_index_; ++i) { // skip symbol 0
|
||||||
|
if (snr[i] > 0) {
|
||||||
|
total_snr += snr[i];
|
||||||
|
snr_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (snr_count > 0) {
|
||||||
|
last_avg_snr_ = 10 * std::log10(total_snr / snr_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract data
|
||||||
|
for (int i = 0; i < data_bits; ++i)
|
||||||
|
CODE::set_le_bit(data, i, mesg[i].v[best] < 0);
|
||||||
|
|
||||||
|
// Descramble
|
||||||
|
CODE::Xorshift32 scrambler;
|
||||||
|
for (int i = 0; i < data_bytes; ++i)
|
||||||
|
data[i] ^= scrambler();
|
||||||
|
|
||||||
|
std::cerr << "Decoder: Frame decoded " << data_bytes << " bytes, SNR=" << last_avg_snr_ << " dB" << std::endl;
|
||||||
|
|
||||||
|
|
||||||
|
callback(data, data_bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
using Encoder48k = ModemEncoder<float, DSP::Complex<float>, 48000>;
|
||||||
|
using Decoder48k = ModemDecoder<float, DSP::Complex<float>, 48000>;
|
||||||
137
rigctl_ptt.hh
Normal file
137
rigctl_ptt.hh
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstring>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
|
||||||
|
class RigctlPTT {
|
||||||
|
public:
|
||||||
|
RigctlPTT(const std::string& host = "localhost", int port = 4532)
|
||||||
|
: host_(host), port_(port) {}
|
||||||
|
|
||||||
|
~RigctlPTT() {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connect() {
|
||||||
|
if (connected_) return true;
|
||||||
|
|
||||||
|
sock_ = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (sock_ < 0) {
|
||||||
|
std::cerr << "rigctl: Failed to create socket" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct hostent* server = gethostbyname(host_.c_str());
|
||||||
|
if (!server) {
|
||||||
|
std::cerr << "rigctl PTT: Can't connect to host " << host_ << std::endl;
|
||||||
|
close(sock_);
|
||||||
|
sock_ = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
memcpy(&addr.sin_addr.s_addr, server->h_addr, server->h_length);
|
||||||
|
addr.sin_port = htons(port_);
|
||||||
|
|
||||||
|
if (::connect(sock_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
std::cerr << "rigctl: Can't connect to " << host_ << ":" << port_ << std::endl;
|
||||||
|
close(sock_);
|
||||||
|
sock_ = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connected_ = true;
|
||||||
|
std::cerr << "rigctl: Connected to " << host_ << ":" << port_ << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void disconnect() {
|
||||||
|
if (sock_ >= 0) {
|
||||||
|
if (ptt_on_) {
|
||||||
|
set_ptt(false);
|
||||||
|
}
|
||||||
|
close(sock_);
|
||||||
|
sock_ = -1;
|
||||||
|
}
|
||||||
|
connected_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bool set_ptt(bool on) {
|
||||||
|
if (!connected_) {
|
||||||
|
if (!connect()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// T 1 (PTT on) or T 0 (PTT off)
|
||||||
|
std::string cmd = on ? "T 1\n" : "T 0\n";
|
||||||
|
|
||||||
|
if (send(sock_, cmd.c_str(), cmd.length(), 0) < 0) {
|
||||||
|
std::cerr << "rigctl: Failed to send PTT command" << std::endl;
|
||||||
|
disconnect();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read response
|
||||||
|
char response[256];
|
||||||
|
int n = recv(sock_, response, sizeof(response) - 1, 0);
|
||||||
|
if (n > 0) {
|
||||||
|
response[n] = '\0';
|
||||||
|
// rigctld returns RPRT 0 on success
|
||||||
|
if (strstr(response, "RPRT 0") || n == 0) {
|
||||||
|
ptt_on_ = on;
|
||||||
|
std::cerr << "rigctl: PTT " << (on ? "ON" : "OFF") << std::endl;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
std::cerr << "rigctl: PTT command failed: " << response << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// temp fallback
|
||||||
|
ptt_on_ = on;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool ptt_on() const { return ptt_on_; }
|
||||||
|
bool is_connected() const { return connected_; }
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string host_;
|
||||||
|
int port_;
|
||||||
|
int sock_ = -1;
|
||||||
|
bool connected_ = false;
|
||||||
|
bool ptt_on_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DummyPTT {
|
||||||
|
public:
|
||||||
|
bool connect() {
|
||||||
|
std::cerr << "PTT: Using dummy PTT (no rigctld)" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void disconnect() {}
|
||||||
|
bool set_ptt(bool on) {
|
||||||
|
ptt_on_ = on;
|
||||||
|
std::cerr << "PTT: " << (on ? "ON" : "OFF") << " (dummy)" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool ptt_on() const { return ptt_on_; }
|
||||||
|
bool is_connected() const { return true; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ptt_on_ = false;
|
||||||
|
};
|
||||||
167
serial_ptt.hh
Normal file
167
serial_ptt.hh
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <termios.h>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
enum class PTTLine {
|
||||||
|
DTR = 0,
|
||||||
|
RTS = 1,
|
||||||
|
BOTH = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
class SerialPTT {
|
||||||
|
public:
|
||||||
|
SerialPTT() = default;
|
||||||
|
|
||||||
|
~SerialPTT() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bool open(const std::string& port, PTTLine line = PTTLine::RTS,
|
||||||
|
bool invert_dtr = false, bool invert_rts = false) {
|
||||||
|
close();
|
||||||
|
|
||||||
|
port_ = port;
|
||||||
|
line_ = line;
|
||||||
|
invert_dtr_ = invert_dtr;
|
||||||
|
invert_rts_ = invert_rts;
|
||||||
|
|
||||||
|
fd_ = ::open(port.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
|
||||||
|
if (fd_ < 0) {
|
||||||
|
last_error_ = std::string("Failed to open ") + port + ": " + strerror(errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct termios tty;
|
||||||
|
if (tcgetattr(fd_, &tty) != 0) {
|
||||||
|
last_error_ = "Failed to get terminal attributes";
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cfmakeraw(&tty);
|
||||||
|
tcsetattr(fd_, TCSANOW, &tty);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ptt_off();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
if (fd_ >= 0) {
|
||||||
|
ptt_off();
|
||||||
|
::close(fd_);
|
||||||
|
fd_ = -1;
|
||||||
|
}
|
||||||
|
open_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool is_open() const { return open_; }
|
||||||
|
|
||||||
|
const std::string& last_error() const { return last_error_; }
|
||||||
|
|
||||||
|
void ptt_on() {
|
||||||
|
if (fd_ < 0) return;
|
||||||
|
|
||||||
|
int flags;
|
||||||
|
if (ioctl(fd_, TIOCMGET, &flags) < 0) return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (line_ == PTTLine::DTR || line_ == PTTLine::BOTH) {
|
||||||
|
if (invert_dtr_) {
|
||||||
|
flags &= ~TIOCM_DTR; // clear DTR
|
||||||
|
} else {
|
||||||
|
flags |= TIOCM_DTR; // set DTR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (line_ == PTTLine::RTS || line_ == PTTLine::BOTH) {
|
||||||
|
if (invert_rts_) {
|
||||||
|
flags &= ~TIOCM_RTS; // clear RTS
|
||||||
|
} else {
|
||||||
|
flags |= TIOCM_RTS; // set RTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ioctl(fd_, TIOCMSET, &flags);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void ptt_off() {
|
||||||
|
if (fd_ < 0) return;
|
||||||
|
|
||||||
|
int flags;
|
||||||
|
if (ioctl(fd_, TIOCMGET, &flags) < 0) return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (line_ == PTTLine::DTR || line_ == PTTLine::BOTH) {
|
||||||
|
if (invert_dtr_) {
|
||||||
|
flags |= TIOCM_DTR; // set DTR
|
||||||
|
} else {
|
||||||
|
flags &= ~TIOCM_DTR; // clear DTR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (line_ == PTTLine::RTS || line_ == PTTLine::BOTH) {
|
||||||
|
if (invert_rts_) {
|
||||||
|
flags |= TIOCM_RTS; // set RTS
|
||||||
|
} else {
|
||||||
|
flags &= ~TIOCM_RTS; // clear RTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ioctl(fd_, TIOCMSET, &flags);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reconnect() {
|
||||||
|
std::string saved_port = port_;
|
||||||
|
PTTLine saved_line = line_;
|
||||||
|
bool saved_invert_dtr = invert_dtr_;
|
||||||
|
bool saved_invert_rts = invert_rts_;
|
||||||
|
|
||||||
|
close();
|
||||||
|
return open(saved_port, saved_line, saved_invert_dtr, saved_invert_rts);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
int fd_ = -1;
|
||||||
|
bool open_ = false;
|
||||||
|
std::string port_;
|
||||||
|
PTTLine line_ = PTTLine::RTS;
|
||||||
|
bool invert_dtr_ = false;
|
||||||
|
bool invert_rts_ = false;
|
||||||
|
std::string last_error_;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue