Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

434 satır
9.9KB

  1. //go:build pluto && linux
  2. package plutosdr
  3. /*
  4. #cgo pkg-config: libiio
  5. #include <iio.h>
  6. #include <stdlib.h>
  7. #include <stdint.h>
  8. */
  9. import "C"
  10. import (
  11. "context"
  12. "fmt"
  13. "log"
  14. "sync"
  15. "sync/atomic"
  16. "time"
  17. "unsafe"
  18. "github.com/jan/fm-rds-tx/internal/output"
  19. "github.com/jan/fm-rds-tx/internal/platform"
  20. )
  21. func updateMaxDuration(dst *atomic.Uint64, d time.Duration) {
  22. v := uint64(d)
  23. for {
  24. cur := dst.Load()
  25. if v <= cur {
  26. return
  27. }
  28. if dst.CompareAndSwap(cur, v) {
  29. return
  30. }
  31. }
  32. }
  33. func durationMs(ns uint64) float64 {
  34. return float64(ns) / float64(time.Millisecond)
  35. }
  36. type PlutoDriver struct {
  37. mu sync.Mutex
  38. cfg platform.SoapyConfig
  39. ctx *C.struct_iio_context
  40. txDev *C.struct_iio_device
  41. phyDev *C.struct_iio_device
  42. chanI *C.struct_iio_channel
  43. chanQ *C.struct_iio_channel
  44. chanLO *C.struct_iio_channel
  45. buf *C.struct_iio_buffer
  46. bufSize int
  47. started bool
  48. configured bool
  49. framesWritten atomic.Uint64
  50. samplesWritten atomic.Uint64
  51. underruns atomic.Uint64
  52. slowWrites atomic.Uint64
  53. slowFills atomic.Uint64
  54. slowPushes atomic.Uint64
  55. maxWriteNs atomic.Uint64
  56. maxFillNs atomic.Uint64
  57. maxPushNs atomic.Uint64
  58. lastError string
  59. lastErrorAt string
  60. layoutLogged bool
  61. }
  62. func NewPlutoDriver() platform.SoapyDriver {
  63. return &PlutoDriver{}
  64. }
  65. func (d *PlutoDriver) Name() string { return "pluto-iio" }
  66. func (d *PlutoDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
  67. d.mu.Lock()
  68. defer d.mu.Unlock()
  69. d.cleanup()
  70. d.cfg = cfg
  71. uri := "usb:"
  72. if cfg.Device != "" && cfg.Device != "plutosdr" {
  73. uri = cfg.Device
  74. }
  75. if v, ok := cfg.DeviceArgs["uri"]; ok && v != "" {
  76. uri = v
  77. }
  78. cURI := C.CString(uri)
  79. defer C.free(unsafe.Pointer(cURI))
  80. ctx := C.iio_create_context_from_uri(cURI)
  81. if ctx == nil {
  82. return fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
  83. }
  84. d.ctx = ctx
  85. txDev := d.findDevice("cf-ad9361-dds-core-lpc")
  86. if txDev == nil {
  87. return fmt.Errorf("pluto: TX device 'cf-ad9361-dds-core-lpc' not found")
  88. }
  89. d.txDev = txDev
  90. phyDev := d.findDevice("ad9361-phy")
  91. if phyDev == nil {
  92. return fmt.Errorf("pluto: PHY device 'ad9361-phy' not found")
  93. }
  94. d.phyDev = phyDev
  95. phyChanTX := d.findChannel(phyDev, "voltage3", true)
  96. if phyChanTX == nil {
  97. phyChanTX = d.findChannel(phyDev, "voltage0", true)
  98. }
  99. if phyChanTX == nil {
  100. return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
  101. }
  102. rate := int64(cfg.SampleRateHz)
  103. if rate < 2084000 {
  104. rate = 2084000
  105. }
  106. d.cfg.SampleRateHz = float64(rate)
  107. if err := d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate); err != nil {
  108. return err
  109. }
  110. bw := rate
  111. if bw > 2000000 {
  112. bw = 2000000
  113. }
  114. if err := d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw); err != nil {
  115. return err
  116. }
  117. phyChanLO := d.findChannel(phyDev, "altvoltage1", true)
  118. d.chanLO = phyChanLO
  119. if phyChanLO != nil {
  120. freqHz := int64(cfg.CenterFreqHz)
  121. if freqHz <= 0 {
  122. freqHz = 100000000
  123. }
  124. if err := d.writeChanAttrLL(phyChanLO, "frequency", freqHz); err != nil {
  125. return err
  126. }
  127. }
  128. attenDB := int64(0)
  129. if cfg.GainDB > 0 {
  130. attenDB = -int64(89 - cfg.GainDB)
  131. if attenDB > 0 {
  132. attenDB = 0
  133. }
  134. if attenDB < -89 {
  135. attenDB = -89
  136. }
  137. }
  138. _ = d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000)
  139. chanI := d.findChannel(txDev, "voltage0", true)
  140. chanQ := d.findChannel(txDev, "voltage1", true)
  141. if chanI == nil || chanQ == nil {
  142. return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
  143. }
  144. C.iio_channel_enable(chanI)
  145. C.iio_channel_enable(chanQ)
  146. d.chanI = chanI
  147. d.chanQ = chanQ
  148. d.bufSize = int(rate) / 20
  149. if d.bufSize < 4096 {
  150. d.bufSize = 4096
  151. }
  152. buf := C.iio_device_create_buffer(txDev, C.size_t(d.bufSize), C.bool(false))
  153. if buf == nil {
  154. return fmt.Errorf("pluto: failed to create TX buffer (size=%d)", d.bufSize)
  155. }
  156. d.buf = buf
  157. d.configured = true
  158. return nil
  159. }
  160. func (d *PlutoDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
  161. return platform.DeviceCaps{
  162. MinSampleRate: 521e3,
  163. MaxSampleRate: 61.44e6,
  164. HasGain: true,
  165. GainMinDB: -89,
  166. GainMaxDB: 0,
  167. Channels: []int{0},
  168. }, nil
  169. }
  170. func (d *PlutoDriver) Start(_ context.Context) error {
  171. d.mu.Lock()
  172. defer d.mu.Unlock()
  173. if !d.configured {
  174. return fmt.Errorf("pluto: not configured")
  175. }
  176. if d.started {
  177. return fmt.Errorf("pluto: already started")
  178. }
  179. d.started = true
  180. return nil
  181. }
  182. func (d *PlutoDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
  183. d.mu.Lock()
  184. buf := d.buf
  185. chanI := d.chanI
  186. chanQ := d.chanQ
  187. started := d.started
  188. bufSize := d.bufSize
  189. d.mu.Unlock()
  190. if !started || buf == nil {
  191. return 0, fmt.Errorf("pluto: not active")
  192. }
  193. if frame == nil || len(frame.Samples) == 0 {
  194. return 0, nil
  195. }
  196. written := 0
  197. total := len(frame.Samples)
  198. for written < total {
  199. chunk := total - written
  200. if chunk > bufSize {
  201. chunk = bufSize
  202. }
  203. step := uintptr(C.iio_buffer_step(buf))
  204. if step == 0 {
  205. return written, fmt.Errorf("pluto: buffer step is 0")
  206. }
  207. ptrI := uintptr(C.iio_buffer_first(buf, chanI))
  208. ptrQ := uintptr(C.iio_buffer_first(buf, chanQ))
  209. if ptrI == 0 || ptrQ == 0 {
  210. return written, fmt.Errorf("pluto: buffer_first returned null")
  211. }
  212. end := uintptr(C.iio_buffer_end(buf))
  213. d.mu.Lock()
  214. if !d.layoutLogged {
  215. delta := int64(ptrQ) - int64(ptrI)
  216. span := int64(0)
  217. if end > ptrI {
  218. span = int64(end - ptrI)
  219. }
  220. 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)
  221. d.layoutLogged = true
  222. }
  223. d.mu.Unlock()
  224. if end > 0 {
  225. bufSamples := int((end - ptrI) / step)
  226. if bufSamples > 0 && chunk > bufSamples {
  227. chunk = bufSamples
  228. }
  229. }
  230. fillStart := time.Now()
  231. for i := 0; i < chunk; i++ {
  232. s := frame.Samples[written+i]
  233. *(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
  234. *(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
  235. ptrI += step
  236. ptrQ += step
  237. }
  238. fillDur := time.Since(fillStart)
  239. pushStart := time.Now()
  240. pushed := int(C.iio_buffer_push(buf))
  241. pushDur := time.Since(pushStart)
  242. chunkBudget := time.Duration(0)
  243. if d.cfg.SampleRateHz > 0 {
  244. chunkBudget = time.Duration(float64(chunk) / d.cfg.SampleRateHz * float64(time.Second))
  245. }
  246. writeDur := fillDur + pushDur
  247. updateMaxDuration(&d.maxFillNs, fillDur)
  248. updateMaxDuration(&d.maxPushNs, pushDur)
  249. updateMaxDuration(&d.maxWriteNs, writeDur)
  250. slow := false
  251. if chunkBudget > 0 {
  252. if fillDur > chunkBudget/4 {
  253. d.slowFills.Add(1)
  254. slow = true
  255. }
  256. if pushDur > chunkBudget/2 {
  257. d.slowPushes.Add(1)
  258. slow = true
  259. }
  260. if writeDur > chunkBudget {
  261. d.slowWrites.Add(1)
  262. slow = true
  263. }
  264. }
  265. if slow {
  266. sw := d.slowWrites.Load()
  267. sf := d.slowFills.Load()
  268. sp := d.slowPushes.Load()
  269. if sw <= 5 || sf <= 5 || sp <= 5 || sw%20 == 0 || sf%20 == 0 || sp%20 == 0 {
  270. log.Printf("pluto-linux slow write: chunk=%d budget=%s fill=%s push=%s total=%s",
  271. chunk, chunkBudget, fillDur, pushDur, writeDur)
  272. }
  273. }
  274. if pushed < 0 {
  275. d.mu.Lock()
  276. d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
  277. d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
  278. d.underruns.Add(1)
  279. d.mu.Unlock()
  280. return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
  281. }
  282. written += chunk
  283. }
  284. d.framesWritten.Add(1)
  285. d.samplesWritten.Add(uint64(written))
  286. return written, nil
  287. }
  288. func (d *PlutoDriver) Stop(_ context.Context) error {
  289. d.mu.Lock()
  290. defer d.mu.Unlock()
  291. d.started = false
  292. return nil
  293. }
  294. func (d *PlutoDriver) Flush(_ context.Context) error { return nil }
  295. func (d *PlutoDriver) Tune(_ context.Context, freqHz float64) error {
  296. d.mu.Lock()
  297. defer d.mu.Unlock()
  298. if !d.configured || d.chanLO == nil {
  299. return fmt.Errorf("pluto: not configured or LO channel not available")
  300. }
  301. return d.writeChanAttrLL(d.chanLO, "frequency", int64(freqHz))
  302. }
  303. func (d *PlutoDriver) Close(_ context.Context) error {
  304. d.mu.Lock()
  305. defer d.mu.Unlock()
  306. d.started = false
  307. d.cleanup()
  308. return nil
  309. }
  310. func (d *PlutoDriver) Stats() platform.RuntimeStats {
  311. d.mu.Lock()
  312. defer d.mu.Unlock()
  313. return platform.RuntimeStats{
  314. TXEnabled: d.started,
  315. StreamActive: d.started && d.buf != nil,
  316. FramesWritten: d.framesWritten.Load(),
  317. SamplesWritten: d.samplesWritten.Load(),
  318. Underruns: d.underruns.Load(),
  319. SlowWrites: d.slowWrites.Load(),
  320. SlowFills: d.slowFills.Load(),
  321. SlowPushes: d.slowPushes.Load(),
  322. LastError: d.lastError,
  323. LastErrorAt: d.lastErrorAt,
  324. EffectiveRate: d.cfg.SampleRateHz,
  325. MaxWriteMs: durationMs(d.maxWriteNs.Load()),
  326. MaxFillMs: durationMs(d.maxFillNs.Load()),
  327. MaxPushMs: durationMs(d.maxPushNs.Load()),
  328. }
  329. }
  330. func (d *PlutoDriver) cleanup() {
  331. if d.buf != nil {
  332. C.iio_buffer_destroy(d.buf)
  333. d.buf = nil
  334. }
  335. if d.chanI != nil {
  336. C.iio_channel_disable(d.chanI)
  337. d.chanI = nil
  338. }
  339. if d.chanQ != nil {
  340. C.iio_channel_disable(d.chanQ)
  341. d.chanQ = nil
  342. }
  343. d.chanLO = nil
  344. if d.ctx != nil {
  345. C.iio_context_destroy(d.ctx)
  346. d.ctx = nil
  347. }
  348. d.txDev = nil
  349. d.phyDev = nil
  350. d.configured = false
  351. d.layoutLogged = false
  352. }
  353. func (d *PlutoDriver) findDevice(name string) *C.struct_iio_device {
  354. if d.ctx == nil {
  355. return nil
  356. }
  357. cName := C.CString(name)
  358. defer C.free(unsafe.Pointer(cName))
  359. return C.iio_context_find_device(d.ctx, cName)
  360. }
  361. func (d *PlutoDriver) findChannel(dev *C.struct_iio_device, name string, isOutput bool) *C.struct_iio_channel {
  362. if dev == nil {
  363. return nil
  364. }
  365. cName := C.CString(name)
  366. defer C.free(unsafe.Pointer(cName))
  367. if isOutput {
  368. return C.iio_device_find_channel(dev, cName, C.bool(true))
  369. }
  370. return C.iio_device_find_channel(dev, cName, C.bool(false))
  371. }
  372. func (d *PlutoDriver) writeChanAttrLL(ch *C.struct_iio_channel, attr string, val int64) error {
  373. if ch == nil {
  374. return fmt.Errorf("pluto: channel missing for attr %s", attr)
  375. }
  376. cAttr := C.CString(attr)
  377. defer C.free(unsafe.Pointer(cAttr))
  378. ret := C.iio_channel_attr_write_longlong(ch, cAttr, C.longlong(val))
  379. if ret < 0 {
  380. return fmt.Errorf("pluto: write attr %s failed (rc=%d)", attr, int(ret))
  381. }
  382. return nil
  383. }