Przeglądaj źródła

feat: add Linux PlutoSDR support and Orange Pi build tooling

tags/v0.9.0
Jan Svabenik 1 miesiąc temu
rodzic
commit
88a1a7736a
6 zmienionych plików z 834 dodań i 29 usunięć
  1. +44
    -0
      docs/config.orangepi-pluto-soapy.json
  2. +1
    -1
      internal/platform/plutosdr/available_pluto.go
  3. +364
    -0
      internal/platform/plutosdr/pluto_linux.go
  4. +1
    -1
      internal/platform/plutosdr/stub.go
  5. +59
    -27
      internal/platform/soapysdr/lib_unix.go
  6. +365
    -0
      scripts/orangepi-build-libiio.sh

+ 44
- 0
docs/config.orangepi-pluto-soapy.json Wyświetl plik

@@ -0,0 +1,44 @@
{
"audio": {
"inputPath": "",
"gain": 1.0,
"toneLeftHz": 1000,
"toneRightHz": 1600,
"toneAmplitude": 0.2
},
"rds": {
"enabled": true,
"pi": "BEEF",
"ps": "PLUTOOPI",
"radioText": "Orange Pi Pluto Soapy test",
"pty": 0
},
"fm": {
"frequencyMHz": 100.0,
"stereoEnabled": true,
"pilotLevel": 0.09,
"rdsInjection": 0.04,
"preEmphasisTauUS": 50,
"outputDrive": 0.5,
"compositeRateHz": 228000,
"maxDeviationHz": 75000,
"limiterEnabled": true,
"limiterCeiling": 1.0,
"fmModulationEnabled": true,
"mpxGain": 1.0,
"bs412Enabled": false,
"bs412ThresholdDBr": 0
},
"backend": {
"kind": "soapy",
"driver": "plutosdr",
"device": "",
"uri": "ip:pluto.local",
"deviceArgs": {},
"outputPath": "",
"deviceSampleRateHz": 2280000
},
"control": {
"listenAddress": "127.0.0.1:8088"
}
}

+ 1
- 1
internal/platform/plutosdr/available_pluto.go Wyświetl plik

@@ -1,4 +1,4 @@
//go:build pluto && windows
//go:build pluto && (windows || linux)


package plutosdr package plutosdr




+ 364
- 0
internal/platform/plutosdr/pluto_linux.go Wyświetl plik

@@ -0,0 +1,364 @@
//go:build pluto && linux

package plutosdr

/*
#cgo pkg-config: libiio
#include <iio.h>
#include <stdlib.h>
#include <stdint.h>
*/
import "C"

import (
"context"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"unsafe"

"github.com/jan/fm-rds-tx/internal/output"
"github.com/jan/fm-rds-tx/internal/platform"
)

type PlutoDriver struct {
mu sync.Mutex
cfg platform.SoapyConfig

ctx *C.struct_iio_context
txDev *C.struct_iio_device
phyDev *C.struct_iio_device
chanI *C.struct_iio_channel
chanQ *C.struct_iio_channel
chanLO *C.struct_iio_channel
buf *C.struct_iio_buffer
bufSize int

started bool
configured bool
framesWritten atomic.Uint64
samplesWritten atomic.Uint64
underruns atomic.Uint64
lastError string
lastErrorAt string
layoutLogged bool
}

func NewPlutoDriver() platform.SoapyDriver {
return &PlutoDriver{}
}

func (d *PlutoDriver) Name() string { return "pluto-iio" }

func (d *PlutoDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
d.mu.Lock()
defer d.mu.Unlock()

d.cleanup()
d.cfg = cfg

uri := "usb:"
if cfg.Device != "" && cfg.Device != "plutosdr" {
uri = cfg.Device
}
if v, ok := cfg.DeviceArgs["uri"]; ok && v != "" {
uri = v
}

cURI := C.CString(uri)
defer C.free(unsafe.Pointer(cURI))
ctx := C.iio_create_context_from_uri(cURI)
if ctx == nil {
return fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
}
d.ctx = ctx

txDev := d.findDevice("cf-ad9361-dds-core-lpc")
if txDev == nil {
return fmt.Errorf("pluto: TX device 'cf-ad9361-dds-core-lpc' not found")
}
d.txDev = txDev

phyDev := d.findDevice("ad9361-phy")
if phyDev == nil {
return fmt.Errorf("pluto: PHY device 'ad9361-phy' not found")
}
d.phyDev = phyDev

phyChanTX := d.findChannel(phyDev, "voltage3", true)
if phyChanTX == nil {
phyChanTX = d.findChannel(phyDev, "voltage0", true)
}
if phyChanTX == nil {
return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
}

rate := int64(cfg.SampleRateHz)
if rate < 2084000 {
rate = 2084000
}
d.cfg.SampleRateHz = float64(rate)
if err := d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate); err != nil {
return err
}

bw := rate
if bw > 2000000 {
bw = 2000000
}
if err := d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw); err != nil {
return err
}

phyChanLO := d.findChannel(phyDev, "altvoltage1", true)
d.chanLO = phyChanLO
if phyChanLO != nil {
freqHz := int64(cfg.CenterFreqHz)
if freqHz <= 0 {
freqHz = 100000000
}
if err := d.writeChanAttrLL(phyChanLO, "frequency", freqHz); err != nil {
return err
}
}

attenDB := int64(0)
if cfg.GainDB > 0 {
attenDB = -int64(89 - cfg.GainDB)
if attenDB > 0 {
attenDB = 0
}
if attenDB < -89 {
attenDB = -89
}
}
_ = d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000)

chanI := d.findChannel(txDev, "voltage0", true)
chanQ := d.findChannel(txDev, "voltage1", true)
if chanI == nil || chanQ == nil {
return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
}
C.iio_channel_enable(chanI)
C.iio_channel_enable(chanQ)
d.chanI = chanI
d.chanQ = chanQ

d.bufSize = int(rate) / 20
if d.bufSize < 4096 {
d.bufSize = 4096
}
buf := C.iio_device_create_buffer(txDev, C.size_t(d.bufSize), C.bool(false))
if buf == nil {
return fmt.Errorf("pluto: failed to create TX buffer (size=%d)", d.bufSize)
}
d.buf = buf
d.configured = true
return nil
}

func (d *PlutoDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
return platform.DeviceCaps{
MinSampleRate: 521e3,
MaxSampleRate: 61.44e6,
HasGain: true,
GainMinDB: -89,
GainMaxDB: 0,
Channels: []int{0},
}, nil
}

func (d *PlutoDriver) Start(_ context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
if !d.configured {
return fmt.Errorf("pluto: not configured")
}
if d.started {
return fmt.Errorf("pluto: already started")
}
d.started = true
return nil
}

func (d *PlutoDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
d.mu.Lock()
buf := d.buf
chanI := d.chanI
chanQ := d.chanQ
started := d.started
bufSize := d.bufSize
d.mu.Unlock()

if !started || buf == nil {
return 0, fmt.Errorf("pluto: not active")
}
if frame == nil || len(frame.Samples) == 0 {
return 0, nil
}

written := 0
total := len(frame.Samples)

for written < total {
chunk := total - written
if chunk > bufSize {
chunk = bufSize
}

step := uintptr(C.iio_buffer_step(buf))
if step == 0 {
return written, fmt.Errorf("pluto: buffer step is 0")
}

ptrI := uintptr(C.iio_buffer_first(buf, chanI))
ptrQ := uintptr(C.iio_buffer_first(buf, chanQ))
if ptrI == 0 || ptrQ == 0 {
return written, fmt.Errorf("pluto: buffer_first returned null")
}

end := uintptr(C.iio_buffer_end(buf))
d.mu.Lock()
if !d.layoutLogged {
delta := int64(ptrQ) - int64(ptrI)
span := int64(0)
if end > ptrI {
span = int64(end - ptrI)
}
log.Printf("pluto-linux: buffer layout step=%d ptrI=%#x ptrQ=%#x delta=%d end=%#x span=%d bufSize=%d", step, ptrI, ptrQ, delta, end, span, bufSize)
d.layoutLogged = true
}
d.mu.Unlock()
if end > 0 {
bufSamples := int((end - ptrI) / step)
if bufSamples > 0 && chunk > bufSamples {
chunk = bufSamples
}
}

for i := 0; i < chunk; i++ {
s := frame.Samples[written+i]
*(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
*(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
ptrI += step
ptrQ += step
}

pushed := int(C.iio_buffer_push(buf))
if pushed < 0 {
d.mu.Lock()
d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
d.underruns.Add(1)
d.mu.Unlock()
return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
}

written += chunk
}

d.framesWritten.Add(1)
d.samplesWritten.Add(uint64(written))
return written, nil
}

func (d *PlutoDriver) Stop(_ context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
d.started = false
return nil
}

func (d *PlutoDriver) Flush(_ context.Context) error { return nil }

func (d *PlutoDriver) Tune(_ context.Context, freqHz float64) error {
d.mu.Lock()
defer d.mu.Unlock()
if !d.configured || d.chanLO == nil {
return fmt.Errorf("pluto: not configured or LO channel not available")
}
return d.writeChanAttrLL(d.chanLO, "frequency", int64(freqHz))
}

func (d *PlutoDriver) Close(_ context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
d.started = false
d.cleanup()
return nil
}

func (d *PlutoDriver) Stats() platform.RuntimeStats {
d.mu.Lock()
defer d.mu.Unlock()
return platform.RuntimeStats{
TXEnabled: d.started,
StreamActive: d.started && d.buf != nil,
FramesWritten: d.framesWritten.Load(),
SamplesWritten: d.samplesWritten.Load(),
Underruns: d.underruns.Load(),
LastError: d.lastError,
LastErrorAt: d.lastErrorAt,
EffectiveRate: d.cfg.SampleRateHz,
}
}

func (d *PlutoDriver) cleanup() {
if d.buf != nil {
C.iio_buffer_destroy(d.buf)
d.buf = nil
}
if d.chanI != nil {
C.iio_channel_disable(d.chanI)
d.chanI = nil
}
if d.chanQ != nil {
C.iio_channel_disable(d.chanQ)
d.chanQ = nil
}
d.chanLO = nil
if d.ctx != nil {
C.iio_context_destroy(d.ctx)
d.ctx = nil
}
d.txDev = nil
d.phyDev = nil
d.configured = false
d.layoutLogged = false
}

func (d *PlutoDriver) findDevice(name string) *C.struct_iio_device {
if d.ctx == nil {
return nil
}
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
return C.iio_context_find_device(d.ctx, cName)
}

func (d *PlutoDriver) findChannel(dev *C.struct_iio_device, name string, isOutput bool) *C.struct_iio_channel {
if dev == nil {
return nil
}
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
if isOutput {
return C.iio_device_find_channel(dev, cName, C.bool(true))
}
return C.iio_device_find_channel(dev, cName, C.bool(false))
}

func (d *PlutoDriver) writeChanAttrLL(ch *C.struct_iio_channel, attr string, val int64) error {
if ch == nil {
return fmt.Errorf("pluto: channel missing for attr %s", attr)
}
cAttr := C.CString(attr)
defer C.free(unsafe.Pointer(cAttr))
ret := C.iio_channel_attr_write_longlong(ch, cAttr, C.longlong(val))
if ret < 0 {
return fmt.Errorf("pluto: write attr %s failed (rc=%d)", attr, int(ret))
}
return nil
}

+ 1
- 1
internal/platform/plutosdr/stub.go Wyświetl plik

@@ -1,4 +1,4 @@
//go:build !pluto || !windows
//go:build !pluto || (!windows && !linux)


package plutosdr package plutosdr




+ 59
- 27
internal/platform/soapysdr/lib_unix.go Wyświetl plik

@@ -4,7 +4,9 @@ package soapysdr


import ( import (
"fmt" "fmt"
"log"
"math" "math"
"sort"
"unsafe" "unsafe"
) )


@@ -29,6 +31,13 @@ static const char* soapy_dlerror() {
return dlerror(); return dlerror();
} }


// Try to resolve SoapySDR_getLastError dynamically when available.
typedef const char* (*last_error_fn)(void);
static const char* call_last_error(void* fn) {
if (fn == NULL) return NULL;
return ((last_error_fn)fn)();
}

// Function call trampolines — we call function pointers loaded via dlsym. // Function call trampolines — we call function pointers loaded via dlsym.
// These avoid the complexity of calling C function pointers from Go directly. // These avoid the complexity of calling C function pointers from Go directly.


@@ -102,22 +111,23 @@ static void call_kwargs_set(void* fn, void* kw, const char* key, const char* val
import "C" import "C"


type soapyLib struct { type soapyLib struct {
handle unsafe.Pointer
fnEnumerate unsafe.Pointer
fnKwargsListClear unsafe.Pointer
fnKwargsSet unsafe.Pointer
fnMake unsafe.Pointer
fnUnmake unsafe.Pointer
fnSetSampleRate unsafe.Pointer
fnSetFrequency unsafe.Pointer
fnSetGain unsafe.Pointer
fnGetGainRange unsafe.Pointer
fnSetupStream unsafe.Pointer
fnCloseStream unsafe.Pointer
fnGetStreamMTU unsafe.Pointer
fnActivateStream unsafe.Pointer
fnDeactivateStream unsafe.Pointer
fnWriteStream unsafe.Pointer
handle unsafe.Pointer
fnEnumerate unsafe.Pointer
fnKwargsListClear unsafe.Pointer
fnKwargsSet unsafe.Pointer
fnMake unsafe.Pointer
fnUnmake unsafe.Pointer
fnSetSampleRate unsafe.Pointer
fnSetFrequency unsafe.Pointer
fnSetGain unsafe.Pointer
fnGetGainRange unsafe.Pointer
fnSetupStream unsafe.Pointer
fnCloseStream unsafe.Pointer
fnGetStreamMTU unsafe.Pointer
fnActivateStream unsafe.Pointer
fnDeactivateStream unsafe.Pointer
fnWriteStream unsafe.Pointer
fnGetLastError unsafe.Pointer
} }


var libNames = []string{ var libNames = []string{
@@ -164,6 +174,7 @@ func loadSoapyLib() (*soapyLib, error) {
fnActivateStream: sym("SoapySDRDevice_activateStream"), fnActivateStream: sym("SoapySDRDevice_activateStream"),
fnDeactivateStream: sym("SoapySDRDevice_deactivateStream"), fnDeactivateStream: sym("SoapySDRDevice_deactivateStream"),
fnWriteStream: sym("SoapySDRDevice_writeStream"), fnWriteStream: sym("SoapySDRDevice_writeStream"),
fnGetLastError: sym("SoapySDR_getLastError"),
}, nil }, nil
} }


@@ -192,20 +203,20 @@ func (lib *soapyLib) enumerate() ([]map[string]string, error) {
} }
}() }()


devices := make([]map[string]string, int(length))
kwSize := unsafe.Sizeof(kwargs{})
devices := make([]map[string]string, 0, int(length))
kwSize := unsafe.Sizeof(C.GoKwargs{})
base := uintptr(ret) base := uintptr(ret)
for i := 0; i < int(length); i++ { for i := 0; i < int(length); i++ {
kw := (*kwargs)(unsafe.Pointer(base + uintptr(i)*kwSize))
entry := (*C.GoKwargs)(unsafe.Pointer(base + uintptr(i)*kwSize))
m := make(map[string]string) m := make(map[string]string)
for j := 0; j < int(kw.size); j++ {
keyPtr := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(kw.keys)) + uintptr(j)*unsafe.Sizeof(uintptr(0))))
valPtr := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(kw.vals)) + uintptr(j)*unsafe.Sizeof(uintptr(0))))
if keyPtr != 0 && valPtr != 0 {
m[C.GoString((*C.char)(unsafe.Pointer(keyPtr)))] = C.GoString((*C.char)(unsafe.Pointer(valPtr)))
for j := 0; j < int(entry.size); j++ {
keyPtr := *(**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(entry.keys)) + uintptr(j)*unsafe.Sizeof(uintptr(0))))
valPtr := *(**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(entry.vals)) + uintptr(j)*unsafe.Sizeof(uintptr(0))))
if keyPtr != nil && valPtr != nil {
m[C.GoString(keyPtr)] = C.GoString(valPtr)
} }
} }
devices[i] = m
devices = append(devices, m)
} }
return devices, nil return devices, nil
} }
@@ -217,9 +228,30 @@ func (lib *soapyLib) makeDevice(driver, device string, args map[string]string) (
var kw kwargs var kw kwargs
if driver != "" { lib.kwargsSet(&kw, "driver", driver) } if driver != "" { lib.kwargsSet(&kw, "driver", driver) }
if device != "" { lib.kwargsSet(&kw, "device", device) } if device != "" { lib.kwargsSet(&kw, "device", device) }
for k, v := range args { lib.kwargsSet(&kw, k, v) }

keys := make([]string, 0, len(args))
for k := range args {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
lib.kwargsSet(&kw, k, args[k])
}

log.Printf("soapy: makeDevice driver=%q device=%q args=%v", driver, device, args)
ret := C.call_make(lib.fnMake, unsafe.Pointer(&kw)) ret := C.call_make(lib.fnMake, unsafe.Pointer(&kw))
if ret == nil { return 0, fmt.Errorf("soapy: failed to open device") }
if ret == nil {
msg := ""
if lib.fnGetLastError != nil {
if p := C.call_last_error(lib.fnGetLastError); p != nil {
msg = C.GoString(p)
}
}
if msg != "" {
return 0, fmt.Errorf("soapy: failed to open device: %s", msg)
}
return 0, fmt.Errorf("soapy: failed to open device")
}
return uintptr(ret), nil return uintptr(ret), nil
} }




+ 365
- 0
scripts/orangepi-build-libiio.sh Wyświetl plik

@@ -0,0 +1,365 @@
#!/usr/bin/env bash
set -Eeuo pipefail

# Orange Pi Plus 2E / Armbian Bookworm build helper for fm-rds-tx
#
# Goals:
# - install build + runtime dependencies
# - install libiio / Pluto-related userspace bits
# - build fmrtx for Linux ARM
# - collect binary + shared libraries into dist/orangepi/
#
# Notes:
# - Linux Pluto build is libiio-first (`-tags pluto`).
# - SoapySDR is optional fallback/debug tooling, not the primary Pluto path.
# - Windows Pluto path remains separate and untouched by this script.
#
# Usage:
# chmod +x scripts/orangepi-build-libiio.sh
# ./scripts/orangepi-build-libiio.sh
#
# Optional env:
# PREFIX=/opt/fm-rds-tx
# DIST_DIR=dist/orangepi
# GO_VERSION=1.22.12
# SKIP_APT=1
# SKIP_GO_INSTALL=1
# BUILD_TAGS=pluto
#
# If you want to install the packaged result into a target directory:
# PREFIX=/opt/fm-rds-tx ./scripts/orangepi-build-libiio.sh

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
DIST_DIR="${DIST_DIR:-${REPO_DIR}/dist/orangepi}"
BUILD_DIR="${DIST_DIR}/build"
RUNTIME_DIR="${DIST_DIR}/runtime"
PREFIX="${PREFIX:-/opt/fm-rds-tx}"
GO_VERSION="${GO_VERSION:-1.22.12}"
BUILD_TAGS="${BUILD_TAGS:-pluto}"
ARCH="$(dpkg --print-architecture 2>/dev/null || true)"

log() {
printf '\n[%s] %s\n' "$(date '+%H:%M:%S')" "$*"
}

need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing required command: $1" >&2
exit 1
}
}

apt_install_if_missing() {
local missing=()
for pkg in "$@"; do
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
missing+=("$pkg")
fi
done
if ((${#missing[@]})); then
log "Installing missing packages: ${missing[*]}"
sudo apt-get install -y "${missing[@]}"
fi
}

install_go() {
if command -v go >/dev/null 2>&1; then
log "Go already present: $(go version)"
return
fi

if [[ "${SKIP_GO_INSTALL:-0}" == "1" ]]; then
echo "go not found and SKIP_GO_INSTALL=1 set" >&2
exit 1
fi

local go_arch
case "$ARCH" in
armhf) go_arch="armv6l" ;;
arm64) go_arch="arm64" ;;
amd64) go_arch="amd64" ;;
*)
echo "Unsupported architecture for automated Go install: $ARCH" >&2
exit 1
;;
esac

local tarball="go${GO_VERSION}.linux-${go_arch}.tar.gz"
local url="https://go.dev/dl/${tarball}"
local tmp="/tmp/${tarball}"

log "Installing Go ${GO_VERSION} for ${go_arch}"
wget -O "$tmp" "$url"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$tmp"
export PATH="/usr/local/go/bin:${PATH}"

if ! grep -q '/usr/local/go/bin' "$HOME/.profile" 2>/dev/null; then
printf '\nexport PATH="/usr/local/go/bin:$PATH"\n' >> "$HOME/.profile"
fi

log "Go installed: $(/usr/local/go/bin/go version)"
}

resolve_lib() {
local name="$1"
local ldconfig_bin=""
if command -v ldconfig >/dev/null 2>&1; then
ldconfig_bin="$(command -v ldconfig)"
elif [[ -x /sbin/ldconfig ]]; then
ldconfig_bin="/sbin/ldconfig"
elif [[ -x /usr/sbin/ldconfig ]]; then
ldconfig_bin="/usr/sbin/ldconfig"
fi

if [[ -n "$ldconfig_bin" ]]; then
"$ldconfig_bin" -p 2>/dev/null | awk -v lib="$name" '$1 == lib { print $NF; exit }'
return 0
fi

find /lib /usr/lib /usr/local/lib -name "$name" 2>/dev/null | head -n 1
}

copy_lib_if_found() {
local libname="$1"
local path
path="$(resolve_lib "$libname" || true)"
if [[ -z "$path" ]]; then
path="$(find /lib /usr/lib /usr/local/lib -name "$libname" 2>/dev/null | head -n 1 || true)"
fi
if [[ -n "$path" && -f "$path" ]]; then
cp -Lv "$path" "$RUNTIME_DIR/lib/"
else
log "Library not found: $libname"
fi
}

find_soapy_plugin_path() {
local candidate=""

if command -v SoapySDRUtil >/dev/null 2>&1; then
candidate="$(SoapySDRUtil --info 2>/dev/null | awk -F': ' '/Search path:/ {print $2; exit}')"
if [[ -n "$candidate" && -d "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
fi

for candidate in \
/usr/local/lib/SoapySDR/modules0.8-3 \
/usr/local/lib/SoapySDR/modules0.8 \
/usr/lib/arm-linux-gnueabihf/SoapySDR/modules0.8-3 \
/usr/lib/arm-linux-gnueabihf/SoapySDR/modules0.8 \
/usr/lib/SoapySDR/modules0.8-3 \
/usr/lib/SoapySDR/modules0.8
do
if [[ -d "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done

return 1
}

copy_soapy_plugins_if_found() {
local plugin_path
plugin_path="$(find_soapy_plugin_path || true)"
if [[ -n "$plugin_path" && -d "$plugin_path" ]]; then
mkdir -p "$RUNTIME_DIR/soapy-modules"
cp -Lv "$plugin_path"/* "$RUNTIME_DIR/soapy-modules/" 2>/dev/null || true
printf '%s\n' "$plugin_path" > "$DIST_DIR/SOAPY_PLUGIN_PATH.txt"
log "Copied Soapy plugins from: $plugin_path"
else
log "Soapy plugin path not found"
fi
}

write_runner() {
cat > "${DIST_DIR}/run-fmrtx.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"

SYSTEM_LIB_DIRS="/usr/local/lib:/usr/lib:/usr/lib/arm-linux-gnueabihf:/lib:/lib/arm-linux-gnueabihf"
export LD_LIBRARY_PATH="${SCRIPT_DIR}/runtime/lib:${SYSTEM_LIB_DIRS}:${LD_LIBRARY_PATH:-}"

if [[ -d "${SCRIPT_DIR}/runtime/soapy-modules" ]]; then
export SOAPY_SDR_PLUGIN_PATH="${SCRIPT_DIR}/runtime/soapy-modules:${SOAPY_SDR_PLUGIN_PATH:-}"
elif [[ -f "${SCRIPT_DIR}/SOAPY_PLUGIN_PATH.txt" ]]; then
export SOAPY_SDR_PLUGIN_PATH="$(cat "${SCRIPT_DIR}/SOAPY_PLUGIN_PATH.txt"):${SOAPY_SDR_PLUGIN_PATH:-}"
fi

exec "${SCRIPT_DIR}/runtime/bin/fmrtx" "$@"
EOF
chmod +x "${DIST_DIR}/run-fmrtx.sh"
}

write_install_helper() {
cat > "${DIST_DIR}/install.sh" <<EOF
#!/usr/bin/env bash
set -euo pipefail
PREFIX="{1:-$PREFIX}"
sudo mkdir -p "\$PREFIX/bin" "\$PREFIX/lib"
sudo cp -v "${DIST_DIR}/runtime/bin/fmrtx" "\$PREFIX/bin/"
sudo cp -v ${DIST_DIR}/runtime/lib/* "\$PREFIX/lib/" 2>/dev/null || true
cat <<'EON'
Installed.
Run with e.g.:
LD_LIBRARY_PATH=\$PREFIX/lib \$PREFIX/bin/fmrtx --help
EON
EOF
# replace placeholder introduced to avoid accidental shell expansion confusion
sed -i 's/{1:-/\${1:-/g' "${DIST_DIR}/install.sh"
chmod +x "${DIST_DIR}/install.sh"
}

main() {
need_cmd bash
need_cmd uname
need_cmd awk
need_cmd sed
need_cmd cp
need_cmd mkdir
need_cmd ldd

mkdir -p "$BUILD_DIR" "$RUNTIME_DIR/bin" "$RUNTIME_DIR/lib"

if [[ "${SKIP_APT:-0}" != "1" ]]; then
log "Refreshing apt metadata"
sudo apt-get update

apt_install_if_missing \
ca-certificates \
curl \
wget \
git \
build-essential \
pkg-config \
gcc \
g++ \
make \
file \
binutils \
tar \
xz-utils \
libiio0 \
libiio-dev \
libusb-1.0-0 \
libxml2 \
libxml2-dev

# Optional / best-effort packages. Not all repos expose them on every arch.
sudo apt-get install -y soapysdr-tools libsoapysdr0.8 libsoapysdr-dev 2>/dev/null || true
sudo apt-get install -y soapy-module-plutosdr 2>/dev/null || true
sudo apt-get install -y iio-oscilloscope 2>/dev/null || true
sudo apt-get install -y libusb-1.0-0-dev 2>/dev/null || true
fi

install_go
need_cmd go

export PATH="/usr/local/go/bin:${PATH}"
export CGO_ENABLED=1
export GOOS=linux

case "$ARCH" in
armhf)
export GOARCH=arm
export GOARM=7
;;
arm64)
export GOARCH=arm64
;;
amd64)
export GOARCH=amd64
;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac

log "Build environment"
go version
echo "ARCH=${ARCH} GOOS=${GOOS} GOARCH=${GOARCH:-} GOARM=${GOARM:-} CGO_ENABLED=${CGO_ENABLED}"

log "Tidying modules"
(cd "$REPO_DIR" && go mod tidy)

log "Building fmrtx with Linux Pluto/libiio-first backend (tags: $BUILD_TAGS)"
(cd "$REPO_DIR" && go build -v -tags "$BUILD_TAGS" -o "$RUNTIME_DIR/bin/fmrtx" ./cmd/fmrtx)

log "Collecting runtime libraries"
copy_lib_if_found "libiio.so.0"
copy_lib_if_found "libSoapySDR.so.0.8"
copy_lib_if_found "libusb-1.0.so.0"
copy_lib_if_found "libxml2.so.2"
copy_lib_if_found "libstdc++.so.6"
copy_lib_if_found "libgcc_s.so.1"
copy_lib_if_found "libm.so.6"
copy_lib_if_found "libc.so.6"
if [[ "$BUILD_TAGS" == *soapy* ]]; then
copy_soapy_plugins_if_found
else
log "Skipping Soapy plugin copy (BUILD_TAGS=$BUILD_TAGS)"
fi

log "Writing helper scripts"
write_runner
write_install_helper

log "Writing build manifest"
cat > "${DIST_DIR}/BUILD-INFO.txt" <<EOF
fm-rds-tx Orange Pi build
========================
Date: $(date -Is)
Host: $(uname -a)
Repo: $REPO_DIR
Dist: $DIST_DIR
Architecture: $ARCH
Go: $(go version)

Build command:
go build -tags $BUILD_TAGS -o runtime/bin/fmrtx ./cmd/fmrtx

Important note:
Linux Pluto builds should prefer the native libiio path (`-tags pluto`).
SoapySDR is optional fallback/debug infrastructure only.
Windows Pluto handling is separate and intentionally untouched by this script.
EOF

log "Binary info"
file "$RUNTIME_DIR/bin/fmrtx" || true

log "Dynamic dependencies of built binary"
ldd "$RUNTIME_DIR/bin/fmrtx" || true

if [[ "$BUILD_TAGS" == *soapy* ]]; then
log "Wrapper self-test: fmrtx --list-devices"
"${DIST_DIR}/run-fmrtx.sh" --list-devices || true
else
log "Wrapper self-test: fmrtx --print-config"
"${DIST_DIR}/run-fmrtx.sh" --print-config || true
fi

log "Done. Artifacts are in: $DIST_DIR"
cat <<EOF

Next steps:
1. Copy/use: $DIST_DIR/runtime/bin/fmrtx
2. Libraries are in: $DIST_DIR/runtime/lib/
3. Launch via wrapper:
$DIST_DIR/run-fmrtx.sh --help
4. Install to target prefix:
$DIST_DIR/install.sh $PREFIX

Reminder:
Default Linux packaging now prefers the native libiio Pluto backend.
Use BUILD_TAGS=soapy only when you explicitly want the Soapy path.
Windows Pluto support is intentionally left on its separate path.
EOF
}

main "$@"

Ładowanie…
Anuluj
Zapisz