Parcourir la source

Add external decoder command scaffolding

master
Jan Svabenik il y a 3 jours
Parent
révision
c0e5fbaa70
6 fichiers modifiés avec 162 ajouts et 12 suppressions
  1. +28
    -2
      cmd/sdrd/main.go
  2. +13
    -0
      internal/config/config.go
  3. +68
    -0
      internal/decoder/decoder.go
  4. +29
    -0
      internal/recorder/decode.go
  5. +17
    -10
      internal/recorder/recorder.go
  6. +7
    -0
      internal/recorder/util.go

+ 28
- 2
cmd/sdrd/main.go Voir le fichier

@@ -294,6 +294,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

decodeMap := buildDecoderMap(cfg)
recMgr := recorder.New(cfg.SampleRate, cfg.FFTSize, recorder.Policy{
Enabled: cfg.Recorder.Enabled,
MinSNRDb: cfg.Recorder.MinSNRDb,
@@ -303,10 +304,11 @@ func main() {
RecordIQ: cfg.Recorder.RecordIQ,
RecordAudio: cfg.Recorder.RecordAudio,
AutoDemod: cfg.Recorder.AutoDemod,
AutoDecode: cfg.Recorder.AutoDecode,
OutputDir: cfg.Recorder.OutputDir,
ClassFilter: cfg.Recorder.ClassFilter,
RingSeconds: cfg.Recorder.RingSeconds,
}, cfg.CenterHz)
}, cfg.CenterHz, decodeMap)

go runDSP(ctx, srcMgr, cfg, det, window, h, eventFile, eventMu, dspUpdates, gpuState, recMgr)

@@ -615,10 +617,11 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
RecordIQ: cfg.Recorder.RecordIQ,
RecordAudio: cfg.Recorder.RecordAudio,
AutoDemod: cfg.Recorder.AutoDemod,
AutoDecode: cfg.Recorder.AutoDecode,
OutputDir: cfg.Recorder.OutputDir,
ClassFilter: cfg.Recorder.ClassFilter,
RingSeconds: cfg.Recorder.RingSeconds,
}, cfg.CenterHz)
}, cfg.CenterHz, buildDecoderMap(cfg))
}
if upd.det != nil {
det = upd.det
@@ -742,6 +745,29 @@ func mustParseDuration(raw string, fallback time.Duration) time.Duration {
return fallback
}

func buildDecoderMap(cfg config.Config) map[string]string {
out := map[string]string{}
if cfg.Decoder.FT8Cmd != "" {
out["FT8"] = cfg.Decoder.FT8Cmd
}
if cfg.Decoder.WSPRCmd != "" {
out["WSPR"] = cfg.Decoder.WSPRCmd
}
if cfg.Decoder.DMRCmd != "" {
out["DMR"] = cfg.Decoder.DMRCmd
}
if cfg.Decoder.DStarCmd != "" {
out["D-STAR"] = cfg.Decoder.DStarCmd
}
if cfg.Decoder.FSKCmd != "" {
out["FSK"] = cfg.Decoder.FSKCmd
}
if cfg.Decoder.PSKCmd != "" {
out["PSK"] = cfg.Decoder.PSKCmd
}
return out
}

func parseSince(raw string) (time.Time, error) {
if raw == "" {
return time.Time{}, nil


+ 13
- 0
internal/config/config.go Voir le fichier

@@ -28,11 +28,21 @@ type RecorderConfig struct {
RecordIQ bool `yaml:"record_iq" json:"record_iq"`
RecordAudio bool `yaml:"record_audio" json:"record_audio"`
AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
OutputDir string `yaml:"output_dir" json:"output_dir"`
ClassFilter []string `yaml:"class_filter" json:"class_filter"`
RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
}

type DecoderConfig struct {
FT8Cmd string `yaml:"ft8_cmd" json:"ft8_cmd"`
WSPRCmd string `yaml:"wspr_cmd" json:"wspr_cmd"`
DMRCmd string `yaml:"dmr_cmd" json:"dmr_cmd"`
DStarCmd string `yaml:"dstar_cmd" json:"dstar_cmd"`
FSKCmd string `yaml:"fsk_cmd" json:"fsk_cmd"`
PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"`
}

type Config struct {
Bands []Band `yaml:"bands" json:"bands"`
CenterHz float64 `yaml:"center_hz" json:"center_hz"`
@@ -46,6 +56,7 @@ type Config struct {
IQBalance bool `yaml:"iq_balance" json:"iq_balance"`
Detector DetectorConfig `yaml:"detector" json:"detector"`
Recorder RecorderConfig `yaml:"recorder" json:"recorder"`
Decoder DecoderConfig `yaml:"decoder" json:"decoder"`
WebAddr string `yaml:"web_addr" json:"web_addr"`
EventPath string `yaml:"event_path" json:"event_path"`
FrameRate int `yaml:"frame_rate" json:"frame_rate"`
@@ -77,9 +88,11 @@ func Default() Config {
RecordIQ: true,
RecordAudio: false,
AutoDemod: true,
AutoDecode: false,
OutputDir: "data/recordings",
RingSeconds: 8,
},
Decoder: DecoderConfig{},
WebAddr: ":8080",
EventPath: "data/events.jsonl",
FrameRate: 15,


+ 68
- 0
internal/decoder/decoder.go Voir le fichier

@@ -0,0 +1,68 @@
package decoder

import (
"errors"
"os/exec"
"strings"
)

type Result struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Code int `json:"code"`
}

// Run executes an external decoder command. If cmdTemplate contains {iq} or {sr}, they are replaced.
// Otherwise, --iq and --sample-rate args are appended.
func Run(cmdTemplate string, iqPath string, sampleRate int) (Result, error) {
if cmdTemplate == "" {
return Result{}, errors.New("decoder command empty")
}
cmdStr := strings.ReplaceAll(cmdTemplate, "{iq}", iqPath)
cmdStr = strings.ReplaceAll(cmdStr, "{sr}", intToString(sampleRate))
parts := strings.Fields(cmdStr)
if len(parts) == 0 {
return Result{}, errors.New("invalid decoder command")
}
cmd := exec.Command(parts[0], parts[1:]...)
if !strings.Contains(cmdTemplate, "{iq}") {
cmd.Args = append(cmd.Args, "--iq", iqPath)
}
if !strings.Contains(cmdTemplate, "{sr}") {
cmd.Args = append(cmd.Args, "--sample-rate", intToString(sampleRate))
}
out, err := cmd.CombinedOutput()
res := Result{Stdout: string(out), Code: 0}
if err != nil {
res.Stderr = err.Error()
res.Code = 1
return res, err
}
return res, nil
}

func intToString(v int) string {
return strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(strings.TrimSpace(strings.TrimSpace((func() string { return string(rune('0')) })()))), ""), ""), "")) + itoa(v)
}

func itoa(v int) string {
if v == 0 {
return "0"
}
n := v
if n < 0 {
n = -n
}
buf := make([]byte, 0, 12)
for n > 0 {
buf = append(buf, byte('0'+n%10))
n /= 10
}
if v < 0 {
buf = append(buf, '-')
}
for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
buf[i], buf[j] = buf[j], buf[i]
}
return string(buf)
}

+ 29
- 0
internal/recorder/decode.go Voir le fichier

@@ -0,0 +1,29 @@
package recorder

import (
"encoding/json"
"path/filepath"

"sdr-visual-suite/internal/decoder"
)

func (m *Manager) runDecodeIfConfigured(mod string, iqPath string, sampleRate int, files map[string]any, dir string) {
if !m.policy.AutoDecode || mod == "" {
return
}
cmd := ""
if m.decodeCommands != nil {
cmd = m.decodeCommands[mod]
}
if cmd == "" {
return
}
res, err := decoder.Run(cmd, iqPath, sampleRate)
if err != nil {
return
}
b, _ := json.MarshalIndent(res, "", " ")
path := filepath.Join(dir, "decode.json")
_ = writeFile(path, b)
files["decode"] = "decode.json"
}

+ 17
- 10
internal/recorder/recorder.go Voir le fichier

@@ -20,34 +20,37 @@ type Policy struct {
RecordIQ bool `yaml:"record_iq" json:"record_iq"`
RecordAudio bool `yaml:"record_audio" json:"record_audio"`
AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
OutputDir string `yaml:"output_dir" json:"output_dir"`
ClassFilter []string `yaml:"class_filter" json:"class_filter"`
RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
}

type Manager struct {
policy Policy
ring *Ring
sampleRate int
blockSize int
centerHz float64
policy Policy
ring *Ring
sampleRate int
blockSize int
centerHz float64
decodeCommands map[string]string
}

func New(sampleRate int, blockSize int, policy Policy, centerHz float64) *Manager {
func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) *Manager {
if policy.OutputDir == "" {
policy.OutputDir = "data/recordings"
}
if policy.RingSeconds <= 0 {
policy.RingSeconds = 8
}
return &Manager{policy: policy, ring: NewRing(sampleRate, blockSize, policy.RingSeconds), sampleRate: sampleRate, blockSize: blockSize, centerHz: centerHz}
return &Manager{policy: policy, ring: NewRing(sampleRate, blockSize, policy.RingSeconds), sampleRate: sampleRate, blockSize: blockSize, centerHz: centerHz, decodeCommands: decodeCommands}
}

func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64) {
func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) {
m.policy = policy
m.sampleRate = sampleRate
m.blockSize = blockSize
m.centerHz = centerHz
m.decodeCommands = decodeCommands
if m.ring == nil {
m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
return
@@ -117,9 +120,10 @@ func (m *Manager) recordEvent(ev detector.Event) error {
return err
}
files := map[string]any{}
var iqPath string
if m.policy.RecordIQ {
path := filepath.Join(dir, "signal.cf32")
if err := writeCF32(path, segment); err != nil {
iqPath = filepath.Join(dir, "signal.cf32")
if err := writeCF32(iqPath, segment); err != nil {
return err
}
files["iq"] = "signal.cf32"
@@ -133,6 +137,9 @@ func (m *Manager) recordEvent(ev detector.Event) error {
return err
}
}
if m.policy.AutoDecode && iqPath != "" && ev.Class != nil {
m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, m.sampleRate, files, dir)
}

return writeMeta(dir, ev, m.sampleRate, files)
}

+ 7
- 0
internal/recorder/util.go Voir le fichier

@@ -0,0 +1,7 @@
package recorder

import "os"

func writeFile(path string, b []byte) error {
return os.WriteFile(path, b, 0o644)
}

Chargement…
Annuler
Enregistrer