pwn 0.5.627 → 0.5.628
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +20 -30
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/lib/pwn/ai/agent/dispatch.rb +76 -4
- data/lib/pwn/ai/agent/learning.rb +124 -1
- data/lib/pwn/ai/agent/loop.rb +104 -12
- data/lib/pwn/ai/agent/metrics.rb +60 -27
- data/lib/pwn/ai/agent/prompt_builder.rb +63 -12
- data/lib/pwn/ai/agent/reflect.rb +41 -7
- data/lib/pwn/ai/agent/registry.rb +69 -4
- data/lib/pwn/ai/ollama.rb +39 -10
- data/lib/pwn/config.rb +11 -0
- data/lib/pwn/ffi/liquid.rb +179 -0
- data/lib/pwn/memory.rb +15 -0
- data/lib/pwn/memory_index.rb +233 -0
- data/lib/pwn/sdr/decoder/bluetooth.rb +141 -75
- data/lib/pwn/sdr/decoder/dect.rb +89 -63
- data/lib/pwn/sdr/decoder/dsp.rb +443 -0
- data/lib/pwn/sdr/decoder/gps.rb +93 -73
- data/lib/pwn/sdr/decoder/gsm.rb +167 -63
- data/lib/pwn/sdr/decoder/lora.rb +118 -65
- data/lib/pwn/sdr/decoder/lte.rb +188 -65
- data/lib/pwn/sdr/decoder/p25.rb +83 -67
- data/lib/pwn/sdr/decoder/zigbee.rb +140 -67
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +1 -0
- data/spec/lib/pwn/memory_index_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +65 -8
- metadata +6 -4
data/lib/pwn/sdr/decoder/dsp.rb
CHANGED
|
@@ -466,6 +466,13 @@ module PWN
|
|
|
466
466
|
public_class_method def self.mag_sq(opts = {})
|
|
467
467
|
iq = opts[:iq]
|
|
468
468
|
n = iq.length / 2
|
|
469
|
+
if native && n >= 64 && PWN::FFI.available?(mod: :Volk)
|
|
470
|
+
begin
|
|
471
|
+
return PWN::FFI::Volk.magnitude_squared(iq: iq)
|
|
472
|
+
rescue StandardError
|
|
473
|
+
# fall through
|
|
474
|
+
end
|
|
475
|
+
end
|
|
469
476
|
out = Array.new(n)
|
|
470
477
|
i = 0
|
|
471
478
|
while i < n
|
|
@@ -567,6 +574,428 @@ module PWN
|
|
|
567
574
|
{ lag: best_lag, score: best_sc }
|
|
568
575
|
end
|
|
569
576
|
|
|
577
|
+
CA_G2_TAPS = {
|
|
578
|
+
1 => [2, 6], 2 => [3, 7], 3 => [4, 8], 4 => [5, 9], 5 => [1, 9],
|
|
579
|
+
6 => [2, 10], 7 => [1, 8], 8 => [2, 9], 9 => [3, 10], 10 => [2, 3],
|
|
580
|
+
11 => [3, 4], 12 => [5, 6], 13 => [6, 7], 14 => [7, 8], 15 => [8, 9],
|
|
581
|
+
16 => [9, 10], 17 => [1, 4], 18 => [2, 5], 19 => [3, 6], 20 => [4, 7],
|
|
582
|
+
21 => [5, 8], 22 => [6, 9], 23 => [1, 3], 24 => [4, 6], 25 => [5, 7],
|
|
583
|
+
26 => [6, 8], 27 => [7, 9], 28 => [8, 10], 29 => [1, 6], 30 => [2, 7],
|
|
584
|
+
31 => [3, 8], 32 => [4, 9]
|
|
585
|
+
}.freeze
|
|
586
|
+
|
|
587
|
+
# ── True-air I/Q chain (all Decoder::* modules) ──────────────────
|
|
588
|
+
|
|
589
|
+
# Supported Method Parameters::
|
|
590
|
+
# out = PWN::SDR::Decoder::DSP.resample_iq(
|
|
591
|
+
# iq: 'required - interleaved [I0,Q0,…] Array<Float>',
|
|
592
|
+
# src_rate: 'required - Hz', dst_rate: 'required - Hz'
|
|
593
|
+
# )
|
|
594
|
+
|
|
595
|
+
public_class_method def self.resample_iq(opts = {})
|
|
596
|
+
iq = opts[:iq]
|
|
597
|
+
src = opts[:src_rate].to_f
|
|
598
|
+
dst = opts[:dst_rate].to_f
|
|
599
|
+
return iq.dup if (src - dst).abs < 1.0 || iq.empty?
|
|
600
|
+
|
|
601
|
+
if native && PWN::FFI.available?(mod: :Liquid)
|
|
602
|
+
begin
|
|
603
|
+
return PWN::FFI::Liquid.resample_iq(iq: iq, rate: dst / src)
|
|
604
|
+
rescue StandardError
|
|
605
|
+
# fall through
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
n_in = iq.length / 2
|
|
609
|
+
ratio = src / dst
|
|
610
|
+
n_out = (n_in / ratio).floor
|
|
611
|
+
out = Array.new(n_out * 2)
|
|
612
|
+
i = 0
|
|
613
|
+
while i < n_out
|
|
614
|
+
pos = i * ratio
|
|
615
|
+
idx = pos.floor
|
|
616
|
+
frac = pos - idx
|
|
617
|
+
2.times do |c|
|
|
618
|
+
a = iq[(idx * 2) + c] || 0.0
|
|
619
|
+
b = iq[((idx + 1) * 2) + c] || a
|
|
620
|
+
out[(i * 2) + c] = a + ((b - a) * frac)
|
|
621
|
+
end
|
|
622
|
+
i += 1
|
|
623
|
+
end
|
|
624
|
+
out
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Supported Method Parameters::
|
|
628
|
+
# out = PWN::SDR::Decoder::DSP.mix_iq(
|
|
629
|
+
# iq: 'required - interleaved I/Q', rate: 'required - Hz',
|
|
630
|
+
# freq: 'required - offset Hz to shift DOWN by'
|
|
631
|
+
# )
|
|
632
|
+
|
|
633
|
+
public_class_method def self.mix_iq(opts = {})
|
|
634
|
+
iq = opts[:iq]
|
|
635
|
+
rate = opts[:rate].to_f
|
|
636
|
+
freq = opts[:freq].to_f
|
|
637
|
+
return iq.dup if freq.abs < 1e-3 || iq.empty?
|
|
638
|
+
|
|
639
|
+
if native && PWN::FFI.available?(mod: :Liquid)
|
|
640
|
+
begin
|
|
641
|
+
return PWN::FFI::Liquid.mix_down(iq: iq, freq: TWO_PI * freq / rate)
|
|
642
|
+
rescue StandardError
|
|
643
|
+
# fall through
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
n = iq.length / 2
|
|
647
|
+
w = TWO_PI * freq / rate
|
|
648
|
+
out = Array.new(n * 2)
|
|
649
|
+
i = 0
|
|
650
|
+
while i < n
|
|
651
|
+
ph = w * i
|
|
652
|
+
c = Math.cos(ph)
|
|
653
|
+
s = Math.sin(ph)
|
|
654
|
+
re = iq[i * 2].to_f
|
|
655
|
+
im = iq[(i * 2) + 1].to_f
|
|
656
|
+
out[i * 2] = (re * c) + (im * s)
|
|
657
|
+
out[(i * 2) + 1] = (im * c) - (re * s)
|
|
658
|
+
i += 1
|
|
659
|
+
end
|
|
660
|
+
out
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Supported Method Parameters::
|
|
664
|
+
# bits = PWN::SDR::Decoder::DSP.gfsk_slice(
|
|
665
|
+
# iq: 'required - interleaved I/Q', rate:, baud:,
|
|
666
|
+
# bt: 'optional - Gaussian BT (default 0.35)', invert: false
|
|
667
|
+
# )
|
|
668
|
+
# GFSK/GMSK/2-FSK: prefer liquid gmskdem at integer sps, else
|
|
669
|
+
# fm_demod_iq → nrz_slice.
|
|
670
|
+
|
|
671
|
+
public_class_method def self.gfsk_slice(opts = {})
|
|
672
|
+
iq = opts[:iq]
|
|
673
|
+
rate = opts[:rate].to_f
|
|
674
|
+
baud = opts[:baud].to_f
|
|
675
|
+
bt = (opts[:bt] || 0.35).to_f
|
|
676
|
+
inv = opts[:invert]
|
|
677
|
+
return [] if iq.empty? || baud <= 0
|
|
678
|
+
|
|
679
|
+
sps = rate / baud
|
|
680
|
+
if native && PWN::FFI.available?(mod: :Liquid) && sps >= 2.0
|
|
681
|
+
begin
|
|
682
|
+
k = sps.round
|
|
683
|
+
riq = (sps - k).abs < 0.02 ? iq : resample_iq(iq: iq, src_rate: rate, dst_rate: baud * k)
|
|
684
|
+
bits = PWN::FFI::Liquid.gmsk_demod(iq: riq, sps: k, bt: bt)
|
|
685
|
+
return inv ? bits.map { |b| b ^ 1 } : bits
|
|
686
|
+
rescue StandardError
|
|
687
|
+
# fall through
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
audio = fm_demod_iq(iq: iq)
|
|
691
|
+
nrz_slice(samples: audio, rate: rate, baud: baud, invert: inv)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Supported Method Parameters::
|
|
695
|
+
# dibits = PWN::SDR::Decoder::DSP.slice_4fsk(
|
|
696
|
+
# samples: 'required - real FM-discriminator baseband',
|
|
697
|
+
# rate:, baud:
|
|
698
|
+
# )
|
|
699
|
+
# → Array<0..3> per symbol (4-level decision, adaptive thresholds).
|
|
700
|
+
# C4FM/4-GFSK maps +3 → 01, +1 → 00, −1 → 10, −3 → 11 in P25/DMR.
|
|
701
|
+
|
|
702
|
+
public_class_method def self.slice_4fsk(opts = {})
|
|
703
|
+
samples = opts[:samples]
|
|
704
|
+
rate = opts[:rate].to_f
|
|
705
|
+
baud = opts[:baud].to_f
|
|
706
|
+
spb = rate / baud
|
|
707
|
+
return [] if spb < 2.0 || samples.empty?
|
|
708
|
+
|
|
709
|
+
lp = envelope_signed(samples: dc_block(samples: samples), window: [(spb / 4.0).round, 1].max)
|
|
710
|
+
nsym = (lp.length / spb).floor
|
|
711
|
+
# sample mid-symbol values, then quantise into 4 levels
|
|
712
|
+
vals = Array.new(nsym) { |i| lp[((i + 0.5) * spb).floor] || 0.0 }
|
|
713
|
+
return [] if vals.empty?
|
|
714
|
+
|
|
715
|
+
sorted = vals.sort
|
|
716
|
+
lo = sorted[(nsym * 0.1).floor] || vals.min
|
|
717
|
+
hi = sorted[(nsym * 0.9).floor] || vals.max
|
|
718
|
+
step = (hi - lo) / 3.0
|
|
719
|
+
step = 1e-9 if step.abs < 1e-9
|
|
720
|
+
t_lo = lo + step
|
|
721
|
+
t_hi = hi - step
|
|
722
|
+
vals.map do |v|
|
|
723
|
+
if v >= t_hi then 1 # +3
|
|
724
|
+
elsif v >= 0 then 0 # +1
|
|
725
|
+
elsif v >= t_lo then 2 # −1
|
|
726
|
+
else 3 # −3
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# Supported Method Parameters::
|
|
732
|
+
# bits = PWN::SDR::Decoder::DSP.manchester_decode(
|
|
733
|
+
# bits: 'required - Array<0|1> at 2×data rate',
|
|
734
|
+
# ieee: 'optional - true = 01→1 10→0 (IEEE 802.3), else 10→1 01→0'
|
|
735
|
+
# )
|
|
736
|
+
|
|
737
|
+
public_class_method def self.manchester_decode(opts = {})
|
|
738
|
+
bits = opts[:bits]
|
|
739
|
+
ieee = opts[:ieee]
|
|
740
|
+
out = []
|
|
741
|
+
i = 0
|
|
742
|
+
while i < bits.length - 1
|
|
743
|
+
a = bits[i]
|
|
744
|
+
b = bits[i + 1]
|
|
745
|
+
if a == b
|
|
746
|
+
i += 1 # phase slip — resync on next transition
|
|
747
|
+
next
|
|
748
|
+
end
|
|
749
|
+
out << (ieee ? b : a)
|
|
750
|
+
i += 2
|
|
751
|
+
end
|
|
752
|
+
out
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Supported Method Parameters::
|
|
756
|
+
# bits = PWN::SDR::Decoder::DSP.diff_decode(bits: Array<0|1>)
|
|
757
|
+
# NRZ-I / DBPSK: output 1 on transition, 0 on hold.
|
|
758
|
+
|
|
759
|
+
public_class_method def self.diff_decode(opts = {})
|
|
760
|
+
bits = opts[:bits]
|
|
761
|
+
prev = bits.first || 0
|
|
762
|
+
out = Array.new(bits.length - 1)
|
|
763
|
+
i = 1
|
|
764
|
+
while i < bits.length
|
|
765
|
+
out[i - 1] = bits[i] == prev ? 0 : 1
|
|
766
|
+
prev = bits[i]
|
|
767
|
+
i += 1
|
|
768
|
+
end
|
|
769
|
+
out
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Supported Method Parameters::
|
|
773
|
+
# bytes = PWN::SDR::Decoder::DSP.bytes_from_bits(
|
|
774
|
+
# bits: Array<0|1>, lsb_first: false
|
|
775
|
+
# )
|
|
776
|
+
|
|
777
|
+
public_class_method def self.bytes_from_bits(opts = {})
|
|
778
|
+
bits = opts[:bits]
|
|
779
|
+
lsb = opts[:lsb_first]
|
|
780
|
+
out = []
|
|
781
|
+
bits.each_slice(8) do |oct|
|
|
782
|
+
next if oct.length < 8
|
|
783
|
+
|
|
784
|
+
v = 0
|
|
785
|
+
if lsb
|
|
786
|
+
oct.each_with_index { |b, i| v |= (b & 1) << i }
|
|
787
|
+
else
|
|
788
|
+
oct.each { |b| v = (v << 1) | (b & 1) }
|
|
789
|
+
end
|
|
790
|
+
out << v
|
|
791
|
+
end
|
|
792
|
+
out
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Supported Method Parameters::
|
|
796
|
+
# crc = PWN::SDR::Decoder::DSP.crc16(
|
|
797
|
+
# bytes: Array<Integer>, poly: 0x1021, init: 0xFFFF,
|
|
798
|
+
# refin: false, refout: false, xorout: 0x0000
|
|
799
|
+
# )
|
|
800
|
+
|
|
801
|
+
public_class_method def self.crc16(opts = {})
|
|
802
|
+
bytes = opts[:bytes]
|
|
803
|
+
poly = (opts[:poly] || 0x1021).to_i
|
|
804
|
+
crc = (opts[:init] || 0xFFFF).to_i
|
|
805
|
+
refin = opts[:refin]
|
|
806
|
+
refout = opts[:refout]
|
|
807
|
+
xorout = (opts[:xorout] || 0).to_i
|
|
808
|
+
bytes.each do |b|
|
|
809
|
+
b = Integer(format('%08b', b & 0xFF).reverse, 2) if refin
|
|
810
|
+
crc ^= (b & 0xFF) << 8
|
|
811
|
+
8.times do
|
|
812
|
+
crc = crc.anybits?(0x8000) ? ((crc << 1) ^ poly) : (crc << 1)
|
|
813
|
+
crc &= 0xFFFF
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
crc = Integer(format('%016b', crc).reverse, 2) if refout
|
|
817
|
+
crc ^ xorout
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# Supported Method Parameters::
|
|
821
|
+
# out = PWN::SDR::Decoder::DSP.whiten_lfsr(
|
|
822
|
+
# bytes: Array<Integer>, poly: Integer, init: Integer, width: 7
|
|
823
|
+
# )
|
|
824
|
+
# Galois LFSR (MSB-first). BLE: poly 0x11 (x^7+x^4+1), init (ch|0x40).
|
|
825
|
+
|
|
826
|
+
public_class_method def self.whiten_lfsr(opts = {})
|
|
827
|
+
bytes = opts[:bytes]
|
|
828
|
+
poly = opts[:poly].to_i
|
|
829
|
+
reg = opts[:init].to_i
|
|
830
|
+
w = (opts[:width] || 7).to_i
|
|
831
|
+
top = 1 << (w - 1)
|
|
832
|
+
out = Array.new(bytes.length)
|
|
833
|
+
bytes.each_with_index do |byte, bi|
|
|
834
|
+
v = byte & 0xFF
|
|
835
|
+
8.times do |i|
|
|
836
|
+
msb = reg.anybits?(top) ? 1 : 0
|
|
837
|
+
reg = ((reg << 1) & ((1 << w) - 1))
|
|
838
|
+
reg ^= poly if msb == 1
|
|
839
|
+
v ^= (msb << i)
|
|
840
|
+
end
|
|
841
|
+
out[bi] = v
|
|
842
|
+
end
|
|
843
|
+
out
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Supported Method Parameters::
|
|
847
|
+
# pulses = PWN::SDR::Decoder::DSP.ook_pulses(
|
|
848
|
+
# iq: 'required - interleaved I/Q', rate:,
|
|
849
|
+
# min_us: 'optional - drop shorter pulses (default 20)'
|
|
850
|
+
# )
|
|
851
|
+
# → Array of { level: 0|1, us: Float, samples: Int } run-length list.
|
|
852
|
+
|
|
853
|
+
public_class_method def self.ook_pulses(opts = {})
|
|
854
|
+
iq = opts[:iq]
|
|
855
|
+
rate = opts[:rate].to_f
|
|
856
|
+
min_us = (opts[:min_us] || 20).to_f
|
|
857
|
+
m2 = mag_sq(iq: iq)
|
|
858
|
+
return [] if m2.length < 32
|
|
859
|
+
|
|
860
|
+
# Adaptive threshold: 0.5·(floor + peak) on power domain
|
|
861
|
+
sorted = m2.sort
|
|
862
|
+
floor = sorted[m2.length / 10] || m2.min
|
|
863
|
+
peak = sorted[(m2.length * 9) / 10] || m2.max
|
|
864
|
+
thr = floor + ((peak - floor) * 0.5)
|
|
865
|
+
us_per = 1_000_000.0 / rate
|
|
866
|
+
runs = []
|
|
867
|
+
state = m2.first >= thr ? 1 : 0
|
|
868
|
+
cnt = 0
|
|
869
|
+
m2.each do |v|
|
|
870
|
+
s = v >= thr ? 1 : 0
|
|
871
|
+
if s == state
|
|
872
|
+
cnt += 1
|
|
873
|
+
else
|
|
874
|
+
runs << { level: state, us: cnt * us_per, samples: cnt } if (cnt * us_per) >= min_us
|
|
875
|
+
state = s
|
|
876
|
+
cnt = 1
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
runs << { level: state, us: cnt * us_per, samples: cnt } if (cnt * us_per) >= min_us
|
|
880
|
+
runs
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Supported Method Parameters::
|
|
884
|
+
# mag = PWN::SDR::Decoder::DSP.cfft_mag(
|
|
885
|
+
# iq: 'required - interleaved I/Q', n: 'optional - FFT size',
|
|
886
|
+
# shift: 'optional - fftshift so DC is centred (default true)'
|
|
887
|
+
# )
|
|
888
|
+
|
|
889
|
+
public_class_method def self.cfft_mag(opts = {})
|
|
890
|
+
iq = opts[:iq]
|
|
891
|
+
n = (opts[:n] || (iq.length / 2)).to_i
|
|
892
|
+
sh = opts.fetch(:shift, true)
|
|
893
|
+
bins =
|
|
894
|
+
if native && PWN::FFI.available?(mod: :FFTW)
|
|
895
|
+
begin
|
|
896
|
+
PWN::FFI::FFTW.cfft(iq: iq, n: n)
|
|
897
|
+
rescue StandardError
|
|
898
|
+
dft_naive(iq: iq, n: n)
|
|
899
|
+
end
|
|
900
|
+
else
|
|
901
|
+
dft_naive(iq: iq, n: n)
|
|
902
|
+
end
|
|
903
|
+
mag = bins.map { |re, im| Math.sqrt((re * re) + (im * im)) }
|
|
904
|
+
sh ? mag.rotate(n / 2) : mag
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
# Naive O(n²) complex DFT — pure-Ruby fallback for small n.
|
|
908
|
+
# Supported Method Parameters::
|
|
909
|
+
# bins = PWN::SDR::Decoder::DSP.dft_naive(iq:, n:)
|
|
910
|
+
|
|
911
|
+
public_class_method def self.dft_naive(opts = {})
|
|
912
|
+
iq = opts[:iq]
|
|
913
|
+
n = (opts[:n] || (iq.length / 2)).to_i
|
|
914
|
+
n = [n, 512].min
|
|
915
|
+
Array.new(n) do |k|
|
|
916
|
+
re = 0.0
|
|
917
|
+
im = 0.0
|
|
918
|
+
j = 0
|
|
919
|
+
while j < n
|
|
920
|
+
ph = -TWO_PI * k * j / n
|
|
921
|
+
c = Math.cos(ph)
|
|
922
|
+
s = Math.sin(ph)
|
|
923
|
+
xr = iq[j * 2].to_f
|
|
924
|
+
xi = iq[(j * 2) + 1].to_f
|
|
925
|
+
re += (xr * c) - (xi * s)
|
|
926
|
+
im += (xr * s) + (xi * c)
|
|
927
|
+
j += 1
|
|
928
|
+
end
|
|
929
|
+
[re, im]
|
|
930
|
+
end
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
# Supported Method Parameters::
|
|
934
|
+
# zc = PWN::SDR::Decoder::DSP.zadoff_chu(root:, n: 63)
|
|
935
|
+
# Returns interleaved [I0,Q0,…] Array<Float>. LTE PSS uses roots 25/29/34.
|
|
936
|
+
|
|
937
|
+
public_class_method def self.zadoff_chu(opts = {})
|
|
938
|
+
u = opts[:root].to_i
|
|
939
|
+
n = (opts[:n] || 63).to_i
|
|
940
|
+
out = Array.new(n * 2)
|
|
941
|
+
n.times do |k|
|
|
942
|
+
ph = -Math::PI * u * k * (k + 1) / n
|
|
943
|
+
out[k * 2] = Math.cos(ph)
|
|
944
|
+
out[(k * 2) + 1] = Math.sin(ph)
|
|
945
|
+
end
|
|
946
|
+
out
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# Supported Method Parameters::
|
|
950
|
+
# chips = PWN::SDR::Decoder::DSP.ca_code(prn: 1..32)
|
|
951
|
+
# Returns Array<Float> of ±1.0 length 1023 (GPS L1 C/A Gold code).
|
|
952
|
+
|
|
953
|
+
public_class_method def self.ca_code(opts = {})
|
|
954
|
+
prn = opts[:prn].to_i
|
|
955
|
+
taps = CA_G2_TAPS[prn]
|
|
956
|
+
raise "ERROR: PRN #{prn} unsupported" unless taps
|
|
957
|
+
|
|
958
|
+
g1 = Array.new(10, 1)
|
|
959
|
+
g2 = Array.new(10, 1)
|
|
960
|
+
out = Array.new(1023)
|
|
961
|
+
1023.times do |i|
|
|
962
|
+
g2i = g2[taps[0] - 1] ^ g2[taps[1] - 1]
|
|
963
|
+
out[i] = (g1[9] ^ g2i) == 1 ? -1.0 : 1.0
|
|
964
|
+
fb1 = g1[2] ^ g1[9]
|
|
965
|
+
fb2 = g2[1] ^ g2[2] ^ g2[5] ^ g2[7] ^ g2[8] ^ g2[9]
|
|
966
|
+
g1.unshift(fb1)
|
|
967
|
+
g1.pop
|
|
968
|
+
g2.unshift(fb2)
|
|
969
|
+
g2.pop
|
|
970
|
+
end
|
|
971
|
+
out
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# Supported Method Parameters::
|
|
975
|
+
# out = PWN::SDR::Decoder::DSP.cmul(a: [I,Q,…], b: [I,Q,…], conj_b: false)
|
|
976
|
+
# Element-wise complex multiply (interleaved). Used for
|
|
977
|
+
# correlation / dechirp: X = A · conj(B).
|
|
978
|
+
|
|
979
|
+
public_class_method def self.cmul(opts = {})
|
|
980
|
+
a = opts[:a]
|
|
981
|
+
b = opts[:b]
|
|
982
|
+
cj = opts[:conj_b]
|
|
983
|
+
n = [a.length, b.length].min / 2
|
|
984
|
+
out = Array.new(n * 2)
|
|
985
|
+
i = 0
|
|
986
|
+
while i < n
|
|
987
|
+
ar = a[i * 2].to_f
|
|
988
|
+
ai = a[(i * 2) + 1].to_f
|
|
989
|
+
br = b[i * 2].to_f
|
|
990
|
+
bi = b[(i * 2) + 1].to_f
|
|
991
|
+
bi = -bi if cj
|
|
992
|
+
out[i * 2] = (ar * br) - (ai * bi)
|
|
993
|
+
out[(i * 2) + 1] = (ar * bi) + (ai * br)
|
|
994
|
+
i += 1
|
|
995
|
+
end
|
|
996
|
+
out
|
|
997
|
+
end
|
|
998
|
+
|
|
570
999
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
571
1000
|
|
|
572
1001
|
public_class_method def self.authors
|
|
@@ -597,6 +1026,20 @@ module PWN
|
|
|
597
1026
|
#{self}.bch_31_21_syndrome(word:)
|
|
598
1027
|
#{self}.baudot_decode(bits:)
|
|
599
1028
|
#{self}.rms_dbfs(samples:) # → Volk (≥64)
|
|
1029
|
+
#{self}.resample_iq(iq:, src_rate:, dst_rate:) # → Liquid crcf
|
|
1030
|
+
#{self}.mix_iq(iq:, rate:, freq:) # → Liquid nco
|
|
1031
|
+
#{self}.gfsk_slice(iq:, rate:, baud:, bt: 0.35) # → Liquid gmskdem
|
|
1032
|
+
#{self}.slice_4fsk(samples:, rate:, baud:) # C4FM/4-GFSK
|
|
1033
|
+
#{self}.manchester_decode(bits:, ieee: false)
|
|
1034
|
+
#{self}.diff_decode(bits:)
|
|
1035
|
+
#{self}.bytes_from_bits(bits:, lsb_first: false)
|
|
1036
|
+
#{self}.crc16(bytes:, poly: 0x1021, init: 0xFFFF)
|
|
1037
|
+
#{self}.whiten_lfsr(bytes:, poly:, init:, width: 7)
|
|
1038
|
+
#{self}.ook_pulses(iq:, rate:, min_us: 20)
|
|
1039
|
+
#{self}.cfft_mag(iq:, n:, shift: true) # → FFTW
|
|
1040
|
+
#{self}.cmul(a:, b:, conj_b: false)
|
|
1041
|
+
#{self}.zadoff_chu(root:, n: 63) # LTE PSS
|
|
1042
|
+
#{self}.ca_code(prn: 1..32) # GPS L1 C/A
|
|
600
1043
|
|
|
601
1044
|
Constants: MORSE_TABLE, BAUDOT_LTRS, BAUDOT_FIGS
|
|
602
1045
|
Backends: PWN::FFI.backends # { Volk: true, Liquid: true, AdalmPluto: true, … }
|
data/lib/pwn/sdr/decoder/gps.rb
CHANGED
|
@@ -3,66 +3,100 @@
|
|
|
3
3
|
module PWN
|
|
4
4
|
module SDR
|
|
5
5
|
module Decoder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# GPS L1 C/A (1575.42 MHz) true-air acquisition.
|
|
7
|
+
#
|
|
8
|
+
# Parallel-code-phase search: 1 ms of I/Q resampled to 2.046 Msps
|
|
9
|
+
# (2 samp/chip), FFT-correlated (via PWN::FFI::FFTW.cfft) against
|
|
10
|
+
# each PRN 1..32 Gold code across ±5 kHz Doppler in 500 Hz steps.
|
|
11
|
+
# Emits {prn:, doppler_hz:, code_phase_chips:, cn0_db_hz:} for every
|
|
12
|
+
# satellite whose peak/next-peak ratio clears threshold — the same
|
|
13
|
+
# cold-start acquisition every GNSS receiver runs, no gnss-sdr binary.
|
|
9
14
|
module GPS
|
|
10
|
-
|
|
15
|
+
CHIP_RATE = 1_023_000
|
|
16
|
+
ACQ_RATE = 2_046_000 # 2 samp/chip → 2046-point FFT
|
|
17
|
+
DOPP_RANGE = 5000
|
|
18
|
+
DOPP_STEP = 500
|
|
19
|
+
ACQ_THRESH = 2.5 # peak / mean ratio
|
|
20
|
+
|
|
21
|
+
# Streaming L1 C/A acquisition demod for Base.run_iq.
|
|
11
22
|
class DemodIQ
|
|
12
|
-
def initialize(rate
|
|
13
|
-
@rate
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
-
@floor = nil
|
|
18
|
-
@in_burst = false
|
|
19
|
-
@burst_t0 = nil
|
|
20
|
-
@peak = -200.0
|
|
21
|
-
@burst_n = 0
|
|
22
|
-
@threshold = (extra[:threshold] || 8.0).to_f
|
|
23
|
+
def initialize(rate:)
|
|
24
|
+
@rate = rate.to_f
|
|
25
|
+
@buf = []
|
|
26
|
+
@seen = {}
|
|
27
|
+
@codes = {}
|
|
23
28
|
end
|
|
24
29
|
|
|
25
|
-
def feed_iq(samples, rate: nil
|
|
30
|
+
def feed_iq(samples, rate: nil)
|
|
26
31
|
@rate = rate.to_f if rate
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
@buf.concat(samples)
|
|
33
|
+
need = (@rate / 1000.0 * 2).ceil * 2 # 1 ms complex + headroom
|
|
34
|
+
return if @buf.length < need * 2
|
|
35
|
+
|
|
36
|
+
ms = PWN::SDR::Decoder::DSP.resample_iq(
|
|
37
|
+
iq: @buf.shift(need * 2), src_rate: @rate, dst_rate: ACQ_RATE
|
|
38
|
+
)[0, 2046 * 2]
|
|
39
|
+
return if ms.nil? || ms.length < 2046 * 2
|
|
40
|
+
|
|
41
|
+
hits = acquire(ms)
|
|
42
|
+
hits.each do |h|
|
|
43
|
+
key = "PRN#{h[:prn]}"
|
|
44
|
+
next if @seen[key] && (@seen[key] - h[:cn0_db_hz]).abs < 1.5
|
|
45
|
+
|
|
46
|
+
@seen[key] = h[:cn0_db_hz]
|
|
47
|
+
yield h
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def code_fft(prn)
|
|
54
|
+
@codes[prn] ||= begin
|
|
55
|
+
chips = PWN::SDR::Decoder::DSP.ca_code(prn: prn)
|
|
56
|
+
# upsample to 2 samp/chip, complex (Q=0)
|
|
57
|
+
iq = Array.new(2046 * 2, 0.0)
|
|
58
|
+
2046.times { |i| iq[i * 2] = chips[i / 2] }
|
|
59
|
+
PWN::FFI.available?(mod: :FFTW) ? PWN::FFI::FFTW.cfft(iq: iq, n: 2046) : PWN::SDR::Decoder::DSP.dft_naive(iq: iq, n: 2046)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def acquire(ms_iq)
|
|
64
|
+
out = []
|
|
65
|
+
(1..32).each do |prn|
|
|
66
|
+
cf = code_fft(prn)
|
|
67
|
+
best = { peak: 0.0, dopp: 0, code: 0, floor: 1.0 }
|
|
68
|
+
(-DOPP_RANGE..DOPP_RANGE).step(DOPP_STEP) do |fd|
|
|
69
|
+
mixed = PWN::SDR::Decoder::DSP.mix_iq(iq: ms_iq, rate: ACQ_RATE, freq: fd)
|
|
70
|
+
sig_f = PWN::FFI.available?(mod: :FFTW) ? PWN::FFI::FFTW.cfft(iq: mixed, n: 2046) : PWN::SDR::Decoder::DSP.dft_naive(iq: mixed, n: 2046)
|
|
71
|
+
# X = FFT(sig) · conj(FFT(code)); corr = |IFFT(X)|
|
|
72
|
+
x_iq = Array.new(2046 * 2)
|
|
73
|
+
2046.times do |k|
|
|
74
|
+
ar, ai = sig_f[k]
|
|
75
|
+
br, bi = cf[k]
|
|
76
|
+
x_iq[k * 2] = (ar * br) + (ai * bi)
|
|
77
|
+
x_iq[(k * 2) + 1] = (ai * br) - (ar * bi)
|
|
43
78
|
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
burst_no: @burst_n, peak_dbfs: @peak.round(1),
|
|
52
|
-
floor_dbfs: @floor.round(1), delta_db: (@peak - @floor).round(1),
|
|
53
|
-
duration_ms: dur_ms, modulation: @modulation,
|
|
54
|
-
sample_rate: @rate.to_i
|
|
55
|
-
}.merge(@extra.except(:threshold))
|
|
56
|
-
# protocol-specific enrichment
|
|
57
|
-
msg.merge!(self.class.enrich(msg)) if self.class.respond_to?(:enrich)
|
|
58
|
-
msg[:summary] = format(
|
|
59
|
-
'%<p>s IQ-burst #%<n>d peak=%<pk>+.1f dBFS Δ=%<d>.1f dB dur=%<ms>d ms',
|
|
60
|
-
p: @protocol, n: @burst_n, pk: @peak, d: @peak - @floor, ms: dur_ms
|
|
61
|
-
)
|
|
62
|
-
yield msg if block_given?
|
|
79
|
+
corr = PWN::FFI.available?(mod: :FFTW) ? PWN::FFI::FFTW.cfft(iq: x_iq, n: 2046, sign: :backward) : PWN::SDR::Decoder::DSP.dft_naive(iq: x_iq, n: 2046)
|
|
80
|
+
mag = corr.map { |re, im| (re * re) + (im * im) }
|
|
81
|
+
pk_i = mag.each_with_index.max_by(&:first).last
|
|
82
|
+
pk = mag[pk_i]
|
|
83
|
+
# exclude ±2 chips around peak for floor estimate
|
|
84
|
+
floor = (mag.sum - mag[[pk_i - 4, 0].max, 9].sum) / (mag.length - 9)
|
|
85
|
+
best = { peak: pk, floor: floor, dopp: fd, code: pk_i } if pk / floor > best[:peak] / [best[:floor], 1e-12].max
|
|
63
86
|
end
|
|
64
|
-
|
|
87
|
+
ratio = best[:peak] / [best[:floor], 1e-12].max
|
|
88
|
+
next unless ratio >= ACQ_THRESH
|
|
89
|
+
|
|
90
|
+
cn0 = 10.0 * Math.log10(ratio * 1000.0) # 1 ms coherent
|
|
91
|
+
out << {
|
|
92
|
+
protocol: 'GPS', event: 'acquisition', modulation: 'BPSK/DSSS',
|
|
93
|
+
prn: prn, doppler_hz: best[:dopp],
|
|
94
|
+
code_phase_chips: (best[:code] / 2.0).round(1),
|
|
95
|
+
peak_to_floor: ratio.round(2), cn0_db_hz: cn0.round(1),
|
|
96
|
+
summary: "GPS PRN#{prn} acquired: Doppler=#{best[:dopp]}Hz code=#{(best[:code] / 2.0).round(1)} C/N0≈#{cn0.round(1)}dB-Hz"
|
|
97
|
+
}
|
|
65
98
|
end
|
|
99
|
+
out
|
|
66
100
|
end
|
|
67
101
|
end
|
|
68
102
|
|
|
@@ -73,23 +107,16 @@ module PWN
|
|
|
73
107
|
|
|
74
108
|
public_class_method def self.decode(opts = {})
|
|
75
109
|
freq_obj = opts[:freq_obj]
|
|
76
|
-
|
|
77
|
-
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_048_000).to_i
|
|
78
|
-
proto = 'GPS'
|
|
79
|
-
demod = DemodIQ.new(
|
|
80
|
-
rate: rate, protocol: proto, modulation: 'BPSK/DSSS',
|
|
81
|
-
extra: { threshold: 3.0 }
|
|
82
|
-
)
|
|
110
|
+
rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_048_000).to_i
|
|
83
111
|
PWN::SDR::Decoder::Base.run_iq(
|
|
84
112
|
freq_obj: freq_obj,
|
|
85
|
-
protocol:
|
|
113
|
+
protocol: 'GPS-L1CA',
|
|
86
114
|
sample_rate: rate,
|
|
87
115
|
source: opts[:source],
|
|
88
116
|
file: opts[:file],
|
|
89
|
-
demod:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
describe: proc { |_b| { modulation: 'BPSK/DSSS', chip_rate: 1_023_000 } }
|
|
117
|
+
demod: DemodIQ.new(rate: rate),
|
|
118
|
+
note: 'BPSK/DSSS 1.023 Mcps — I/Q→FFT parallel-code-phase acquisition (PWN::FFI::FFTW) → PRN/Doppler/C-N0.',
|
|
119
|
+
describe: proc { |_b| { modulation: 'BPSK/DSSS', chip_rate: CHIP_RATE } }
|
|
93
120
|
)
|
|
94
121
|
end
|
|
95
122
|
|
|
@@ -101,15 +128,8 @@ module PWN
|
|
|
101
128
|
out[:lat] = ::Regexp.last_match(1) if line =~ /Lat(?:itude)?\s*=\s*(-?[\d.]+)/i
|
|
102
129
|
out[:lon] = ::Regexp.last_match(1) if line =~ /Long(?:itude)?\s*=\s*(-?[\d.]+)/i
|
|
103
130
|
out[:alt] = ::Regexp.last_match(1) if line =~ /Height\s*=\s*(-?[\d.]+)/i
|
|
104
|
-
out[:utc] = ::Regexp.last_match(1) if line =~ /UTC\s*=?\s*([\d:.-]+T?[\d:.]*)/
|
|
105
131
|
out[:nmea] = line if line.start_with?('$G')
|
|
106
|
-
out[:summary] =
|
|
107
|
-
"GPS FIX #{out[:lat]},#{out[:lon]} alt=#{out[:alt]}m"
|
|
108
|
-
elsif out[:prn]
|
|
109
|
-
"GPS PRN#{out[:prn]} CN0=#{out[:cn0]}"
|
|
110
|
-
else
|
|
111
|
-
"GPS #{line[0, 100]}"
|
|
112
|
-
end
|
|
132
|
+
out[:summary] = out[:lat] ? "GPS FIX #{out[:lat]},#{out[:lon]}" : "GPS PRN#{out[:prn]} CN0=#{out[:cn0]}"
|
|
113
133
|
out.compact
|
|
114
134
|
end
|
|
115
135
|
|
|
@@ -124,7 +144,7 @@ module PWN
|
|
|
124
144
|
#{self}.decode(
|
|
125
145
|
freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
|
|
126
146
|
source: 'optional - :auto|:rtlsdr|:adalm_pluto|:file',
|
|
127
|
-
file: 'optional - .cu8/.cs16 capture'
|
|
147
|
+
file: 'optional - .cu8/.cs16 capture (≥2.048 Msps)'
|
|
128
148
|
)
|
|
129
149
|
|
|
130
150
|
#{self}.authors
|