Meteor Forward Scatter (GRAVES 143.05 MHz):
DIY Receiving Station

This guide explains how to build and configure a station to detect meteors or bolides using forward scatter of the GRAVES radar signal. It includes hardware setup, antenna orientation, FFT/audio configuration, and SpectrumLab automation.

1. Hardware & Connection

ComponentRecommendedPurpose
SDRAirspy MiniReceives 143.05 MHz carrier
LNA + FilterNooelec SAWbird+ 2 mImproves SNR and filters interference
Antenna144 MHz Yagi 4 elements (vertical)Captures forward-scattered echoes
ComputerN100 mini computer (Windows 11)
SoftwareSpectrumLab (+ optional Colorgramme)Processes signal and logs events

Antenna orientation (example of Girona)

Connection eschema

Hardware connection diagram

2. FFT & Audio Input Settings

ParameterValueNotes
Center frequency143.050 MHzGRAVES carrier
ModeAMCarrier inspection
Airspy Sample Rate3 MS/sHigh resolution front-end
Decimate64Noise reduction + resolution
FFT Size32768 binsHigh spectral detail
Effective bandwidth≈ 468 kHz30e6 / 64
FFT resolution≈ 1.43 Hz30e6 / 64 / 32768

Increase gain until the GRAVES carrier is barely visible (around −60 /- 70 dB), not clipped. In noisy environments, use a 144 MHz band-pass filter like Nooelec SAWbird+ 2 m.

3. SpectrumLab Conditional Actions

Based on Conditional Actions published by Ian Lauwerys (Link) . Save on a text file and import in Conditional Actions setup (Spectrum Lab)

; Exported "Conditional Actions" for Spectrum Lab

if( never ) then ##### DEFINITIONS #####################################################################################################################################
if( never ) then # ====== User definable options for meteor logging and capture ======
if( never ) then # SNR_Threshold - Minimum SNR in dB to trigger an event, increase if noise is being interpreted as meteors, reduce if meteors are not being logged
if( never ) then # Meteor_Gap - the period that the signal has to drop below the threshold to be considered the end of a meteor event (in seconds)
if( never ) then # Log_Threshold - the minimum duration of a meteor event that will be logged in seconds, logs everything if less than FFT window time (length)
if( never ) then # Waterfall_Length - the width of the waterfall display in seconds, subtract 20% to be sure of screenshot containing start and end of events
if( never ) then # Sound_Capture - set to 1 to capture meteor sounds, 0 to disable sound capture
if( never ) then # Min_BW_Hz - Minimum bandwith of the meteor signal. Set as small as possible without having false positives.
if( initialising ) then SNR_Threshold = 18 : Meteor_Gap = 1.0: Log_Threshold = 0.2 : Waterfall_Length = 30 : Sound_Capture = 0 : Min_BW_Hz = 15
if( never ) then # Log_Path - the location to log files for meteor events (remember to double escape backslashes, including trailing backslash)
if( never ) then # Capture_Path - the location to save screen captures of meteor events (format as above)
if( never ) then # Sound_Path - the location to save .WAV sound capture of meteor events (format as above)
if( never ) then # SpecLab_Path - the location where you have installed Spectrum Lab (format as above)
if( initialising ) then Log_Path = "z:\\Spectrum\\logfiles\\" : Capture_Path = "z:\\Spectrum\\screenshots\\" : Sound_Path = "z:\\Spectrum\\sounds\\" : SpecLab_Path = "c:\\Spectrum\\"
if( never ) then # ====== User definable options for file upload, all methods ======
if( never ) then # Upload_Capture - set to 1 to upload image captures
if( never ) then # Upload_Log - set to 1 to upload log files
if( never ) then # Upload_Threshold - minimum SNR in dB to trigger uploading an 'interesting' screen capture. Increase or decrease as per SNR_Threshold
if( never ) then # Upload_Limit - number of "interesting" screen captures to rotate through, e.g. set to 9 to keep and upload interesting_1.jpg ... interesting_9.jpg.
if( never ) then # Upload_Method - set to ftp or sftp or s3  Important: Copy the upload_ftp.bat, upload_sftp.bat or upload_s3.bat file in to the Spectrum Lab folder!
if( never ) then # Upload_Directory - the location to upload to, either a directory or an S3 folder, include leading and trailing slashes, e.g. /var/www/html/uploads/ or /meteorfolder/
if( never ) then # Hostname_Bucket - the host name or S3 bucket to upload your files to e.g. ftp.example.com or meteorbucket
if( initialising ) then Upload_Capture = 0 : Upload_Log = 0 : Upload_Threshold = 30 : Upload_Limit = 9 : Upload_Method = "s3" : Upload_Directory = "/meteordata/" : Hostname_Bucket = "meteorbucket"
if( never ) then # ====== User definable options for FTP and SFTP upload - see comments in upload_ftp.bat and upload_sftp.bat files! ======
if( never ) then # FTP_Username - the user name for your FTP server e.g. ftpuploaduser
if( never ) then # FTP_Password - the password for your FTP server e.g. ftpuploadpassword (unfortunately non-encrypted!) (not required if using SFTP certificate authentication)
if( initialising ) then FTP_Username = "ftpuploaduser" : FTP_Password = "ftpuploadpassword"
if( never ) then # ====== User definable options for SFTP upload - see comments in upload_sftp.bat file! ======
if( never ) then # SFTP_Hostkey - the identifier of the key to use to athenticate, e.g. ssh-ed25519 255 ZtOO/gesm31OpRu2S9A9jtrbD4bO9LcGobxhvzLyUBE= (Example is for WinSCP)
if( never ) then # SFTP_PrivateKey - path and filename of the private keypair file, c:\\Users\\ExampleUser\\Example-Keypair.ppk (Example is for WinSCP)
if( initialising ) then SFTP_Hostkey = "ssh-ed25519 255 ZtOO/gesm31OpRu2S9A9jtrbD4bO9LcGobxhvzLyUBE=" : SFTP_PrivateKey = "c:\\Users\\ExampleUser\\Example-Keypair.ppk"
if( never ) then # ====== User definable options for S3 upload - see comments in upload_s3.bat file! ======
if( never ) then # S3_ACL - set to public-read to make files accessible via https or private to limit access to authorised S3 users
if( initialising ) then S3_ACL = "public-read"
if( never ) then # ====== End Of User definable options ======
if( never ) then ##### INITIALISATION ##################################################################################################################################
if( never ) then # Initialise Variables
if( initialising ) then Current_Noise = noise(cfg.SpecFreqMin, cfg.SpecFreqMax) * 6 : Current_Signal = peak_a(cfg.SpecFreqMin, cfg.SpecFreqMax): Current_Signal = 0 : Current_SNR = 0 : Current_Peak_Frequency = 0 : Current_Time = 0
if( initialising ) then Meteor_Start_Time = 0 : Meteor_End_Time = 0 : Daily_Meteor_Count = 0 : Midnight_Flag = 0 : Hourly_Meteor_Count = 0 : Hourly_Meteor_Duration = 0 : Hourly_Meteor_Longest = 0 : Hourly_Flag = 0 : Logging = 1 : Capture_Time = 0
if( initialising ) then Meteor_Max_Signal = 0 : Meteor_Max_Noise = 0 : Meteor_Max_SNR = 0: Meteor_Max_Peak = 0 : Meteor_Count = 0 : lastSNR = 0 : Meteor_Power=0: Last_Power=0
if( initialising ) then Intermediate_Capture = Waterfall_Length : State = "Waiting" : Queued = 0
if( initialising ) then Current_Upload = 1 : Queued_Interesting_Capture = 0 : Do_Interesting_Capture = 0 : Do_Log_Upload = 0
if( initialising ) then Upload_Command = SpecLab_Path + "upload_" + Upload_Method + ".bat " : Capture_Name = ""
if( initialising ) then Upload_Parameters = " \"" + Hostname_Bucket + "\" \"" + Upload_Directory + "\" \"" + FTP_Username + "\" \"" + FTP_Password + "\" \"" + SFTP_Hostkey + "\" \"" + SFTP_PrivateKey + "\" \"" + S3_ACL + "\""
if( never ) then ##### STATE: WAITING ##################################################################################################################################
if( never ) then # Measure current signal, exponential weighted average of noise and peak frequency after each FFT calculation
if( new_spectrum ) then Current_Noise = (Current_Noise / 6) * 5 + noise(cfg.SpecFreqMin, cfg.SpecFreqMax) : Current_Signal = peak_a(cfg.SpecFreqMin, cfg.SpecFreqMax) : Current_Peak_Frequency = peak_f(cfg.SpecFreqMin, cfg.SpecFreqMax) : Current_SNR = Current_Signal - (Current_Noise / 6) : Current_Time = time
if( new_spectrum ) then BW_Window = Min_BW_Hz / 2 : AvgBW = avrg(Current_Peak_Frequency - BW_Window, Current_Peak_Frequency + BW_Window) : AvgNarrow = avrg(Current_Peak_Frequency - 1, Current_Peak_Frequency + 1) : Band_OK = (AvgBW >= (AvgNarrow - 3))
if( State="Waiting" & Current_SNR>=SNR_Threshold & Band_OK ) then State = "Meteor" : Meteor_Start_Time = Current_Time : rec.filename = Sound_Path + "event" + str("YYYYMMDD", now) + "_" + str("hhmmss", now) + "_" + str(Daily_Meteor_Count + 1) + ".wav" : Meteor_Power = Meteor_Power-avrg(143049500, 143050500)
if( never ) then ##### STATE: METEOR ###################################################################################################################################
if( never ) then # SNR is higher than previous max SNR, record new max (subtraction gives SNR in dB)
if( State="Meteor" & Current_SNR>=Meteor_Max_SNR ) then Meteor_Max_Signal = Current_Signal : Meteor_Max_Noise = (Current_Noise / 6) : Meteor_Max_Peak = Current_Peak_Frequency : Meteor_Max_SNR = Current_SNR : rec.trigger = Sound_Capture
if( never ) then # Meteor in progress, signal below threshold so start timing gap in case of short break in meteor signal
if( State="Meteor" & Current_SNR=SNR_Threshold & Band_OK ) then State = "Meteor"
if( never ) then # Meteor hasn't finished, but start or middle of event needs to be captured before it scrolls off
if( (State="Gap" | State="Meteor") & (Current_Time - Meteor_Start_Time)>=Intermediate_Capture & Logging ) then Intermediate_Capture = Intermediate_Capture + Waterfall_Length : capture(Capture_Path + "event" + str("YYYYMMDD", now) + "_" + str("hhmmss", now) + "_" + str(Daily_Meteor_Count + 1) + ".jpg", 100)
if( never ) then # Timing gap, signal below threshold and gap duration exceeded so log meteor event
if( State="Gap" & Current_SNR=Meteor_Gap ) then State = "Log" : rec.trigger = 0
if( never ) then ##### STATE: LOG ######################################################################################################################################
if( never ) then # Log last meteor event
if( State="Log" & (Meteor_End_Time - Meteor_Start_Time)>=Log_Threshold & Logging ) then Daily_Meteor_Count = Daily_Meteor_Count + 1 : Hourly_Meteor_Count = Hourly_Meteor_Count + 1 : Hourly_Meteor_Duration = Hourly_Meteor_Duration + (Meteor_End_Time - Meteor_Start_Time)
if( continuation ) then Queued = Queued + 1 : queue_event(now + max(1, (Waterfall_Length - (Meteor_End_Time - Meteor_Start_Time))), 0): lastSNR = int(Meteor_Max_SNR): Last_Power = Meteor_Power : Meteor_Power = 0
if( continuation ) then fopen(Log_Path + "event_log_" + str("YYYYMM", Meteor_Start_Time) + ".csv",a,r)
if( continuation ) then fp(str("YYYY/MM/DD",Meteor_Start_Time) + "," + str("hh:mm:ss",Meteor_Start_Time) + "," + str(Daily_Meteor_Count) + "," + str(Meteor_Max_Signal) + "," + str(Meteor_Max_Noise) + "," + str(Meteor_Max_SNR) + "," + str(Meteor_Max_Peak) + "," + str(Meteor_End_Time - Meteor_Start_Time))
if( continuation ) then fclose
if( never ) then sp.print(t=now+3, fn="Microsoft Sans Serif", fs=14, fc=0xFFFFFF, fa=0, "                                     Event: " + str(Daily_Meteor_Count) + "  Start: " + str("hh:mm:ss", Meteor_Start_Time) + " UTC  Dur: " + str(Meteor_End_Time - Meteor_Start_Time) + "s  Peak SNR: " + Meteor_Max_SNR + "dB")
if( never ) then # Last meteor event is "interesting" so flag for an extra capture (only works for first event if multiple are queued)
if( State="Log" & Meteor_Max_SNR>=Upload_Threshold & Logging & Upload_Capture ) then Queued_Interesting_Capture = 1
if( never ) then # Reset and wait for next meteor event
if( State="Log" & Hourly_Meteor_Longest<(Meteor_End_Time - Meteor_Start_Time) ) then Hourly_Meteor_Longest = (Meteor_End_Time - Meteor_Start_Time)
if( State="Log" ) then Intermediate_Capture = Waterfall_Length : Meteor_Max_Signal = 0 : Meteor_Max_Noise = 0 : Meteor_Max_SNR = 0: Meteor_Max_Peak = 0
if( continuation ) then State = "Waiting"
if( never ) then ##### QUEUED ACTION TRIGGERED #########################################################################################################################
if( never ) then # Capture a screenshot of the display at end of event (see also Screen Capture tab)
if( queued_event ) then capture(Capture_Path + "event" + str("YYYYMMDD", now) + "_" + str("hhmmss", now) + "_" + str(Meteor_End_Time - Meteor_Start_Time) + "_" + str(int(Last_Power)) + ".jpg", 100) : Do_Interesting_Capture = Queued_Interesting_Capture
if( continuation ) then Queued = Queued - 1
if( never ) then # Capture a second screenshot of the display for upload
if( never ) then # Upload command format: upload_.bat         
if( Do_Interesting_Capture=1 ) then Capture_Name="interesting_" + str(Current_Upload) + ".jpg" : capture(Capture_Path + Capture_Name, 100) : Do_Interesting_Capture = 0 : Queued_Interesting_Capture = 0
if( continuation ) then exec(Upload_Command + "\"" + Capture_Path + "\" \"" + Capture_Name + "\"" + Upload_Parameters) : Current_Upload = Current_Upload + 1
if( never ) then # Roll over to overwrite the first interesting capture file
if( Current_Upload>Upload_Limit ) then Current_Upload = 1
if( never ) then ##### TIMED ACTION TRIGGERED: HOURLY ##################################################################################################################
if( str("mmss", Current_Time)="5955" ) then Hourly_Flag = Hourly_Flag + 1
if( never ) then # If the hourly flag has just been triggered, log and then reset the meteor count to zero for the new hour, if the hourly flag has been triggered already, do nothing
if( Hourly_Flag=1 ) then fopen2(Log_Path + "hourly_log_" + str("YYYYMM", Current_Time) + ".csv",a,r)
if( continuation ) then fp2(str("YYYY/MM/DD", Current_Time) + "," + str("hh", Current_Time) + "," + str(Hourly_Meteor_Count))
if( continuation ) then fclose2
if( continuation ) then fopen4(Log_Path + "RMOB-" + str("YYYYMM", Current_Time) + ".dat",a,r)
if( continuation ) then fp4(str("YYYYMMDDhh", Current_Time) + "," + str("hh", Current_Time) + "," + str(Hourly_Meteor_Count))
if( continuation ) then fclose4
if( continuation ) then fopen5(Log_Path + "RMOB_Dur-" + str("YYYYMM", Current_Time) + ".dat",a,r)
if( continuation ) then fp5(str("YYYYMMDDhh", Current_Time) + "," + str("hh", Current_Time) + "," + str(Hourly_Meteor_Count) + "," + str(Hourly_Meteor_Duration) + "," + str(Hourly_Meteor_Longest) + "," + str(Current_Noise))
if( continuation ) then fclose5
if( continuation ) then Hourly_Meteor_Count = 0 : Hourly_Meteor_Duration = 0 : Hourly_Meteor_Longest = 0
if( never ) then # If the current time is on the hour, reset the hourly flag ready to trigger again at the end of the next hour
if( str("mmss", Current_Time)="0000" ) then Hourly_Flag = 0
if( never ) then ##### TIMED ACTION TRIGGERED: DAILY ###################################################################################################################
if( never ) then # Increment the midnight flag (this will happen multiple times during the last five seconds of the day)
if( str("hhmmss", Current_Time)="235955" ) then Midnight_Flag = Midnight_Flag + 1
if( never ) then # If the midnight flag has just been triggered, log and then reset the meteor count to zero for the new day, if the midnight flag has been triggered already, do nothing
if( Midnight_Flag=1 ) then fopen3(Log_Path + "daily_log_" + str("YYYYMM", Current_Time) + ".csv",a,r)
if( continuation ) then fp3(str("YYYY/MM/DD", Current_Time) + "," + str(Daily_Meteor_Count))
if( continuation ) then fclose3
if( continuation ) then Daily_Meteor_Count = 0 : Do_Log_Upload = Current_Time
if( never ) then # We have a set of logs to upload (by reference to their date in Do_Log_Upload) and upload logs is true, upload them and then reset flag
if( Do_Log_Upload<>0 && Upload_Log=1 ) then exec(Upload_Command + "\"" + Log_Path + "\" \"event_log_" + str("YYYYMM", Do_Log_Upload) + ".csv\"" + Upload_Parameters)
if( continuation ) then exec(Upload_Command + "\"" + Log_Path + "\" \"hourly_log_" + str("YYYYMM", Do_Log_Upload) + ".csv\"" + Upload_Parameters)
if( continuation ) then exec(Upload_Command + "\"" + Log_Path + "\" \"RMOB-" + str("YYYYMM", Do_Log_Upload) + ".dat\"" + Upload_Parameters)
if( continuation ) then exec(Upload_Command + "\"" + Log_Path + "\" \"RMOB_Dur-" + str("YYYYMM", Do_Log_Upload) + ".dat\"" + Upload_Parameters)
if( continuation ) then exec(Upload_Command + "\"" + Log_Path + "\" \"daily_log_" + str("YYYYMM", Do_Log_Upload) + ".csv\"" + Upload_Parameters)
if( continuation ) then Do_Log_Upload=0
if( never ) then # We have a set of logs to upload but upload logs is false so reset flag only
if( Do_Log_Upload<>0 && Upload_Log=0 ) then Do_Log_Upload=0
if( never ) then # If the current time is midnight, reset the midnight flag ready to trigger again at the end of the next day
if( str("hhmmss", Current_Time)="000000" ) then Midnight_Flag = 0
if( never ) then #######################################################################################################################################################
You must have to edit/adjust file paths and the following parameters
ParameterValueMeaningOperationTuning
SNR_Threshold18 dBMinimum SNR for event detectionStarts “Meteor” if SNR ≥ threshold15–24 dB depending on noise
Meteor_Gap1.0 sAllowed low-signal gap within event<1 s = same event, ≥1 s = end0.5–1.0 s typical
Log_Threshold0.2 sMinimum duration to recordShorter echoes ignored0.1–0.3 s adjust per FFT
Waterfall_Length30 sWaterfall time span / screenshot windowUsed for automatic captures30–60 s if long events
Sound_Capture0Enable (1) or disable (0) audio recording0 = no .wav filesUse 1 for acoustic study

Generated files

4. Adapting to Other SDRs / Noise Levels

5. License

This guide and its graphics (except third-party logos or referenced images) are licensed under Creative Commons Attribution – NonCommercial – ShareAlike 4.0 (CC BY-NC-SA 4.0). You may copy, modify, and share it for non-commercial purposes with attribution and share-alike terms.
creativecommons.org/licenses/by-nc-sa/4.0