浏览代码

feat: add simulated transmit path to main cli

tags/v0.3.0-pre
Jan Svabenik 1 个月前
父节点
当前提交
822ff6d357
共有 5 个文件被更改,包括 103 次插入0 次删除
  1. +14
    -0
      cmd/fmrtx/main.go
  2. +6
    -0
      docs/README.md
  3. +56
    -0
      internal/app/sim.go
  4. +26
    -0
      internal/app/sim_test.go
  5. +1
    -0
      scripts/check.ps1

+ 14
- 0
cmd/fmrtx/main.go 查看文件

@@ -6,7 +6,9 @@ import (
"log"
"net/http"
"os"
"time"

apppkg "github.com/jan/fm-rds-tx/internal/app"
cfgpkg "github.com/jan/fm-rds-tx/internal/config"
ctrlpkg "github.com/jan/fm-rds-tx/internal/control"
drypkg "github.com/jan/fm-rds-tx/internal/dryrun"
@@ -17,6 +19,9 @@ func main() {
printConfig := flag.Bool("print-config", false, "print effective config and exit")
dryRun := flag.Bool("dry-run", false, "run no-hardware dry-run output")
dryOutput := flag.String("dry-output", "-", "dry-run output path or - for stdout")
simulate := flag.Bool("simulate-tx", false, "run simulated Soapy/backend transmit path")
simulateOutput := flag.String("simulate-output", "", "simulated transmit output file")
simulateDuration := flag.Duration("simulate-duration", 500*time.Millisecond, "simulated transmit duration")
flag.Parse()

cfg, err := cfgpkg.Load(*configPath)
@@ -40,6 +45,15 @@ func main() {
return
}

if *simulate {
summary, err := apppkg.RunSimulatedTransmit(cfg, *simulateOutput, *simulateDuration)
if err != nil {
log.Fatalf("simulate-tx failed: %v", err)
}
fmt.Println(summary)
return
}

srv := ctrlpkg.NewServer(cfg)
log.Printf("fm-rds-tx listening on %s", cfg.Control.ListenAddress)
log.Fatal(http.ListenAndServe(cfg.Control.ListenAddress, srv.Handler()))


+ 6
- 0
docs/README.md 查看文件

@@ -7,6 +7,7 @@
- `go run ./cmd/fmrtx -print-config`
- `go run ./cmd/fmrtx -config docs/config.sample.json`
- `go run ./cmd/fmrtx --dry-run --dry-output build/dryrun/frame.json`
- `go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms`
- `go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32`

### Internal DSP module
@@ -25,6 +26,11 @@ It is intended as a no-hardware smoke path for the CLI and config/control-adjace

The HTTP control plane also exposes `GET /dry-run` for quick inspection of the currently effective no-hardware summary.

## Simulated transmit

`--simulate-tx` runs the offline generator through the Soapy-oriented simulated backend path and writes an IQ-style artifact to disk.
This is the current closest no-hardware stand-in for the future transmit pipeline in the main application path.

## Offline generation

`cmd/offline` generates a deterministic no-hardware IQ/composite-style file using the repository's output backend path.


+ 56
- 0
internal/app/sim.go 查看文件

@@ -0,0 +1,56 @@
package app

import (
"context"
"encoding/binary"
"fmt"
"path/filepath"
"time"

cfgpkg "github.com/jan/fm-rds-tx/internal/config"
offpkg "github.com/jan/fm-rds-tx/internal/offline"
"github.com/jan/fm-rds-tx/internal/output"
"github.com/jan/fm-rds-tx/internal/platform"
)

func RunSimulatedTransmit(cfg cfgpkg.Config, outPath string, duration time.Duration) (string, error) {
if outPath == "" {
outPath = filepath.Join("build", "sim", "simulated-soapy.iqf32")
}

fileBackend, err := output.NewFileBackend(outPath, binary.LittleEndian, output.BackendInfo{
Name: "simulated-soapy-file",
Description: "simulated soapy sink to file",
})
if err != nil {
return "", err
}
defer fileBackend.Close(context.Background())

soapyCfg := platform.SoapyConfig{
BackendConfig: output.BackendConfig{
SampleRateHz: float64(cfg.FM.CompositeRateHz),
Channels: 2,
IQLevel: float32(cfg.FM.OutputDrive),
},
Driver: "simulated",
Device: cfg.Backend.Device,
CenterFreqHz: cfg.FM.FrequencyMHz * 1_000_000,
Simulated: true,
SimulationPath: outPath,
}
backend := platform.NewSoapyBackend(soapyCfg, platform.NewSimulatedDriver(fileBackend))
if err := backend.Configure(context.Background(), soapyCfg.BackendConfig); err != nil {
return "", err
}

gen := offpkg.NewGenerator(cfg)
frame := gen.GenerateFrame(duration)
if _, err := backend.Write(context.Background(), frame); err != nil {
return "", err
}
if err := backend.Flush(context.Background()); err != nil {
return "", err
}
return fmt.Sprintf("simulated transmit: backend=%s output=%s duration=%s", backend.Info().Name, outPath, duration), nil
}

+ 26
- 0
internal/app/sim_test.go 查看文件

@@ -0,0 +1,26 @@
package app

import (
"os"
"path/filepath"
"strings"
"testing"
"time"

cfgpkg "github.com/jan/fm-rds-tx/internal/config"
)

func TestRunSimulatedTransmit(t *testing.T) {
cfg := cfgpkg.Default()
out := filepath.Join(t.TempDir(), "simulated.iqf32")
summary, err := RunSimulatedTransmit(cfg, out, 20*time.Millisecond)
if err != nil {
t.Fatalf("RunSimulatedTransmit failed: %v", err)
}
if !strings.Contains(summary, "simulated transmit") {
t.Fatalf("unexpected summary: %s", summary)
}
if info, err := os.Stat(out); err != nil || info.Size() == 0 {
t.Fatalf("expected non-empty output file, err=%v", err)
}
}

+ 1
- 0
scripts/check.ps1 查看文件

@@ -1,6 +1,7 @@
$ErrorActionPreference = "Stop"
go test ./...
go run ./cmd/fmrtx --dry-run --dry-output build/dryrun/frame.json
go run ./cmd/fmrtx --simulate-tx --simulate-output build/sim/simulated-soapy.iqf32 --simulate-duration 250ms
go run ./cmd/offline -duration 500ms -output build/offline/composite.iqf32
Push-Location internal
go test ./...


正在加载...
取消
保存