Imports system Imports System.net Imports System.Net.sockets Imports System.Windows.Forms.Application Imports System.Windows.Forms.Timer Imports System.Buffer Imports System.Threading.Thread Imports System.IO Imports System.math Namespace RSS Public Class RSSBase ''' Format string specifying how the file name is constructed from a DateTime object. Public Shared fileformat As String = "yyyyMMddTHHmmss" Public Const majorversion As Integer = 2 Public Const minorversion As Integer = 2 Public Const minorminorversion As Integer = 20 Public c As ColorRes = ColorRes.twelvebit Friend Shared ff As Byte = 255 Friend Shared fefe() As Byte = New Byte() {254, 254} Public Shared Function RSSDate(Optional ByVal x As DateTime = #12:00:00 AM#) As Double Const timezero As DateTime = #12/30/1899# If x = #12:00:00 AM# Then Return Now.Subtract(timezero).TotalDays Else Return x.Subtract(timezero).TotalDays End If End Function ''' Minimum value in an array. Public Overloads Shared Function minimum(ByVal data() As Short) As Short Dim m As Short = Short.MaxValue For i As Integer = 0 To data.Length - 1 If data(i) < m Then m = data(i) Next Return m End Function ''' Maximum value in an array. Public Overloads Shared Function maximum(ByVal data() As Short) As Short Dim m As Short = Short.MinValue For i As Integer = 0 To data.Length - 1 If data(i) > m Then m = data(i) Next Return m End Function Public Shared Function dB(ByVal x As Single, Optional ByVal power As Boolean = True) As Single If power Then Return 10 * Log10(x) Else Return 20 * Log10(x) End Function Public Shared Function datafilename(ByVal path As String, _ ByVal curtime As DateTime, Optional ByVal ext As String = ".sps") As String Dim d As String = path If String.IsNullOrEmpty(path) Then d = curtime.ToString("yyMM") Else d = My.Computer.FileSystem.CombinePath(d, curtime.ToString("yyMM")) End If If Not My.Computer.FileSystem.DirectoryExists(d) Then My.Computer.FileSystem.CreateDirectory(d) End If d = My.Computer.FileSystem.CombinePath(d, curtime.ToString("yyMMdd")) If Not My.Computer.FileSystem.DirectoryExists(d) Then My.Computer.FileSystem.CreateDirectory(d) End If d = My.Computer.FileSystem.CombinePath(d, curtime.ToString("yyMMddhhmmss") & ext) Return d End Function Public Shared Sub interleave(ByVal data() As Double, ByVal buffer() As Double) ' reorder the array to interleaved Dim count As Integer = data.Length \ 2 Dim c1 As Integer = 0 Dim c2 As Integer = count Dim c3 As Integer = 0 For i As Integer = 0 To count - 1 buffer(c3) = data(c1) : c1 += 1 : c3 += 1 buffer(c3) = data(c2) : c2 += 1 : c3 += 1 Next End Sub Public Shared Sub interleave(ByVal data() As Single, ByVal buffer() As Single) ' reorder the array to interleaved Dim count As Integer = data.Length \ 2 Dim c1 As Integer = 0 Dim c2 As Integer = count Dim c3 As Integer = 0 For i As Integer = 0 To count - 1 buffer(c3) = data(c1) : c1 += 1 : c3 += 1 buffer(c3) = data(c2) : c2 += 1 : c3 += 1 Next End Sub End Class ''' ''' SPS is a .Net class that writes spectrogram files readable by Jim Sky's Radio Sky Spectrograph ''' (RSS, at radiosky.com), ''' which records and displays radio spectra from radio observatories. These files consist of headers ''' containing observatory information and basic parameters describing the one table they contain. SPS files ''' can contain data for up to two receivers, but only one is supported as yet. ''' ''' Usage is relatively simple. One creates an instance, fills in the metadata, acquires the data, ''' writes the header, writes the data, and finally closes the file. The example code is in VB.Net. ''' ' create the object ''' Dim countofspectralchannels As Integer = ... ' number of spectral channels to be recorded ''' Dim countofspectra as Integer = ... ' number of spectra to be recorded ''' Dim spsfile As New SPS(countofspectra, countofspectralchannels, SPS.colorres.twelvebit) ''' ''' With spsfile ''' ' fill in the metadata ''' ''' ' observatory information ''' .latitude = "..." ' as double ''' .longitude = "..." ' in degrees east ''' .observatory = "..." ' name of the observatory ''' .location = "..." ' place ''' .observer = "..." ' my name ''' ''' ' other data ''' .freqcent = ... ' center frequency in Herz of each spectrum ''' .freqspan = ... ' the span of frequency of each spectrum ''' .datestart = DateTime.UTCNow ' the starting time of the spectrogram ''' ''' ' acquire the spectrogram, spectrum by spectrum; quit when full ''' Do Until .isfull ''' Dim spectrum () as Short = ... ''' .spectrum = spectrum ''' Loop ''' ''' ' alternative: acquire the spectrogram spectrum by spectrum from a float array ''' ' an offset is added, then the result is scaled ''' Do Until .isfull() OrElse quitflag ''' Dim spectrum () as Single = ... ''' .spectrum(offset, scale) = spectrum ''' Loop ''' ''' ' it is possible to allocate for additional or fewer spectra ''' .naxis1 = .naxis1 + 10000 ' additional spectra ''' .naxis1(.naxis2+5) = naxis1+100 ' both number of spectra and number of spectral channels ''' ''' .dateend = DateTime.UTCNow ' the ending time of the data ''' ''' ' write the file ''' .openFile(nameoffile, optionalpath) ' open the file ''' .writeHeader() ' write the header ''' .writeData() ' write the data ''' .close() ' close the file ''' End With ''' ''' The two spectrum methods can be combined, but it is up to the user to write the ''' number of spectra to the file that matches the .naxis1 count previously written to the header. ''' The number of spectra actually acquired can be less than countofspectra. When ''' writeHeader() later is called, the correct count will be written to the header. ''' Public Class SPSWriter Inherits rssbase ''' The time of the first spectrum. Public dateobs As DateTime ''' The time of the last spectrum. Public dateend As DateTime ''' The observatory's name. Public observatory As String = Nothing ''' The observatory's location. Public location As String = Nothing ''' Observatory latitude. Public latitude As Double = 0 ' latitude ''' Observatory longitude Public longitude As Double = 0 ' longitude ''' Observatory elevation (not used). Public elevation As Double = 0 ' meters ''' Time zone relative to GMT of the observatory. Public timezone As Short = 0 ''' Observer's name. Public observer As String = Nothing ''' File author's name (not used). Public author As String = Nothing ''' The number of receivers interleaved in the spectra. Public receivercount As Integer = 1 ''' The number of spectral channels in each spectrum. Public naxis2 As Short ' count of channels ''' The center frequency of each spectrum Public freqcent As Double ' center frequency of the spectra ''' The span of frequencies of each spectrum. Public freqspan As Double ' frequency span of the spectra ''' The number of bits of data in each spectral channel. ''' Unipolar types interpret data as spectral with .sps extension; ''' bipolar as non-spectral with .dat extension. Private bitmax As Short Private bitmin As Short Private ext As String Public data()() As Short ''' Whether data are written into a linear buffer (false) vs. circular buffer (true). ''' The property isfull can be true only for a linear buffer. Public circular As Boolean = False Friend fid As FileStream Friend binWriter As BinaryWriter ' must be set after object is created Public obs As ObservatoryData ''' The constructor. ''' The number of spectra to be written. Used to preallocate space ''' for the data. The actual number acquired can be less, in which case writeHeader detects ''' and accommodates the actual number. ''' The number of spectral channels in each spectrum (default 1). ''' The number of bits in each spectral channel (default 10 bits). ''' Whether the instance is to record spectra as a circular buffer. Public Sub New(ByVal numberofspectra As Integer, _ ByVal numberofspectralchannels As Integer, _ ByVal circularmode As Boolean, _ Optional ByVal wordsize As ColorRes = ColorRes.twelvebit, _ Optional ByVal reccount As Integer = 1) receivercount = reccount c = wordsize circular = circularmode ' set both naxis1 and naxis2, and allocate data naxis1(numberofspectralchannels) = numberofspectra ' set the data word size limits Select Case c Case ColorRes.tenbit : bitmax = 1023 : bitmin = 0 : ext = ".sps" Case ColorRes.twelvebit : bitmax = 4095 : bitmin = 0 : ext = ".sps" Case ColorRes.bipolar : bitmax = 32767 : bitmin = -32768 : ext = ".dat" Case Else ' error End Select End Sub ''' Opens the file. ''' The name of the file to be written to including the path. ''' If no file name is specified, then standardfilename() is used. Public Sub OpenFile(Optional ByVal filename As String = Nothing, Optional ByVal path As String = Nothing) ' output file If String.IsNullOrEmpty(filename) Then filename = standardfilename() Dim x As String If String.IsNullOrEmpty(path) Then x = filename Else x = My.Computer.FileSystem.CombinePath(path, filename) fid = New FileStream(x, FileMode.Create) binWriter = New BinaryWriter(fid) End Sub ''' Provides a file name based on the current date and time (UTC). ''' String of the form yyyyMMddTHHmmss.sps derived from the current time UTC. ''' If the data are bipolar as set by the ColorRes setting, the file-name extension is .dat, otherwise it is .sps. Public Overloads Function standardfilename() Return DateTime.UtcNow.ToString(fileformat) & ext End Function ''' Another file name derived from a date. ''' A file time from which to derive the UTC file name. ''' The optional path. ''' If the data are bipolar as set by the ColorRes setting, the file-name extension is .dat, otherwise it is .sps. Public Overloads Function standardfilename(ByVal t As DateTime, Optional ByVal path As String = Nothing) Dim fn As String = DateTime.FromFileTimeUtc(t.Ticks).ToString(fileformat) & ext If Not String.IsNullOrEmpty(path) Then fn = My.Computer.FileSystem.CombinePath(path, fn) Return fn End Function ''' Writes the SPS file header. ''' The metadata must have previously been provided. Public Sub WriteHeader(ByVal obs As ObservatoryData.observatory) If Not IsNothing(obs) AndAlso Not IsNothing(obs) Then With obs observatory = .observatory observer = .observer author = .observer timezone = .timezone latitude = .latitude longitude = .longitude location = .Location End With End If ' check for not full If Not circular AndAlso Not isfull Then naxis1 = currentspectrum ' fixed-length portion of the header ' major, minor, minor-minor version binWriter.Write(sprintf("%05d%02d%03d", majorversion, minorversion, minorminorversion).ToCharArray) ' start and end date and time; don't know why the extra day is needed binWriter.Write(RSSDate(dateobs)) binWriter.Write(RSSDate(dateend)) ' latitude 0x1a binWriter.Write(latitude) ' longitude 0x22 binWriter.Write(longitude) ' unused 0x2a binWriter.Write(CDbl(0)) binWriter.Write(CDbl(0)) ' time zone 0x3a binWriter.Write(timezone) ' unused 10-byte field 0x3c binWriter.Write(("0000000000").ToCharArray) ' author 0x46 binWriter.Write(author.PadRight(20).ToCharArray) ' observatory name 0x5a binWriter.Write(observatory.PadRight(20).ToCharArray) ' observatory location 0x6e binWriter.Write(location.PadRight(40).ToCharArray) ' frequency channels 0x96 0x96 binWriter.Write(naxis2) ' note (often contains metadata) Dim note(7) As String note(0) = sprintf("*[[*SWEEPS%d", naxis1) note(1) = sprintf("LOWF%d", CInt(freqcent - freqspan / 2)) note(2) = sprintf("HIF%d", CInt(freqcent + freqspan / 2)) note(3) = sprintf("STEPS%d", naxis2) note(4) = "DUALSPECFILE" & (receivercount <> 1).ToString note(5) = sprintf("RCVR%d", receivercount) ' BANNER0 ' BANNER1 ' ANTENNATYPE ' ANTENNAELEVATION ' ANTENNAPOLARIZATION0 ' ANTENNAPOLARIZATION1 ' COLORFILE ' COLOROFFSET0 ' COLOROFFSET1 if dual receivers ' COLORGAIN0 ' COLORGAIN1 if dual receivers ' if correction file is loaded ' CORRECTIONFILENAME ' CAXFn freq, n indexed through the correction array ' CAS1n correction array element, n indexed through the correction array ' CLOCKMSGn clock metadata elements note(6) = sprintf("COLORRES%d", c) note(7) = sprintf("*]]*") ' write the length of the note 0x98 Dim notelength As Integer = 0 For i As Integer = 0 To note.Length - 1 notelength += note(i).Length Next binWriter.Write(notelength + 8) ' write the note itself 0x9c For i As Integer = 0 To note.Length - 1 binWriter.Write(note(i).ToCharArray) binWriter.Write(ff) Next End Sub ''' Writes the spectrogram in data to the file. ''' The metadata must have previously been provided. Public Sub WriteData() Static buf(2 * receivercount * naxis2 - 1) As Byte If Not circular AndAlso currentspectrum < data.Length Then naxis1 = currentspectrum Dim d2 As Integer = 2 * receivercount * naxis2 For i As Integer = currentspectrum To currentspectrum + naxis1 - 1 ' write the array BlockCopy(data(i Mod naxis1), 0, buf, 0, d2) ' swap bytes Array.Reverse(buf, 0, d2) ' write to the file binWriter.Write(buf, 0, d2) ' terminator characters binWriter.Write(fefe) Next End Sub ''' Closes the file. ''' Once this method is called, the instance can no longer be used. Public Sub close() If fid IsNot Nothing Then fid.Flush() fid.Close() fid = Nothing End If If binWriter IsNot Nothing Then binWriter.Close() binWriter = Nothing End If End Sub ''' Writes the file. Combines OpenFile, WriteHeader, WriteData, and close. ''' Name of the file to which to write. ''' Path of the file to which to write. Public Sub export(ByVal obs As ObservatoryData.observatory, Optional ByVal filename As String = Nothing, Optional ByVal path As String = Nothing) OpenFile(filename, path) WriteHeader(obs) WriteData() close() End Sub ''' Minimum value in the data member. Public Overloads Function minimum() As Short Dim m As Short = Short.MaxValue For i As Integer = 0 To data.Length - 1 Dim x As Short = minimum(data(i)) If x < m Then m = x Next Return m End Function ''' Maximum value in the data member. Public Overloads Function maximum() As Short Dim m As Short = Short.MinValue For i As Integer = 0 To data.Length - 1 Dim x As Short = maximum(data(i)) If x > m Then m = x Next Return m End Function Private currentspectrum As Integer = 0 ''' Writes a spectrum to the file. ''' The spectrum to be written. ''' When circular is false and the number of spectra written exceeds the number defined when the instance was created. ''' Keeps track of how many spectra have been acquired. When circular is false, see isfull(). ''' When circular is true, isfull is always false, and writeData writes to the file ''' the entire data array starting from currentspectrum and wrapping around the end ''' when requested. Public Overloads WriteOnly Property spectrum() As Short() Set(ByVal value As Short()) If Not circular AndAlso currentspectrum >= naxis1 Then Throw (New ApplicationException(sprintf("%s.spectrum - too many spectra written: %d (max %d).", Me.GetType.Name, currentspectrum + 1, naxis1))) Else Array.Copy(value, data(currentspectrum), value.Length) currentspectrum += 1 End If If circular Then currentspectrum = currentspectrum Mod naxis1 End Set End Property ''' Writes a spectrum to the file. ''' An offset applied to the spectrum prior to scaling and short conversion. ''' A scale factor applied to the spectrum after the offset and before short conversion. ''' When the number of spectra acquired exceeds the number defined when the instance was created. ''' The acquired spectrum. ''' Keeps track of how many spectra have been acquired. When circular is false, see isfull(). ''' When circular is true, isfull is always false, and writeData writes to the file ''' the entire data array starting from currentspectrum and wrapping around the end ''' when requested. Public Overloads WriteOnly Property spectrum(ByVal offset As Single, ByVal scale As Single) As Single() Set(ByVal value() As Single) If Not circular AndAlso currentspectrum >= naxis1 Then Throw (New ApplicationException(sprintf("%s.spectrum - too many spectra written: %d (max %d).", Me.GetType.Name, currentspectrum + 1, naxis1))) Else ' convert to short Dim x() As Short = data(currentspectrum) ' straight intensity scale Dim s As Single = scale * (bitmax - bitmin) Dim b1 As Single = CDbl(bitmin) Dim b2 As Single = CDbl(bitmax) For i As Integer = 0 To value.Length - 1 x(i) = CShort(Min(Max(b1, s * (value(i) + offset)), b2)) Next End If currentspectrum += 1 If circular Then currentspectrum = currentspectrum Mod naxis1 End Set End Property ''' Writes a spectrum to the file. ''' An offset applied to the spectrum prior to scaling and short conversion. ''' A scale factor applied to the spectrum after the offset and before short conversion. ''' When the number of spectra acquired exceeds the number defined when the instance was created. ''' The acquired spectrum. ''' Keeps track of how many spectra have been acquired. When circular is false, see isfull(). ''' When circular is true, isfull is always false, and writeData writes to the file ''' the entire data array starting from currentspectrum and wrapping around the end ''' when requested. Public Overloads WriteOnly Property spectrum(ByVal offset As Double, ByVal scale As Double) As Double() Set(ByVal value() As Double) If Not circular AndAlso currentspectrum >= naxis1 Then Throw (New ApplicationException(sprintf("%s.spectrum - too many spectra written: %d (max %d).", Me.GetType.Name, currentspectrum + 1, naxis1))) End If ' more than one receiver If receivercount > 1 Then Static buf() As Double If buf Is Nothing OrElse buf.Length <> value.Length Then ReDim buf(value.Length - 1) ' interleave the data RSSBase.interleave(value, buf) value = buf End If ' convert to short Dim x() As Short = data(currentspectrum) Dim s As Double = scale * (bitmax - bitmin) Dim b1 As Double = CDbl(bitmin) Dim b2 As Double = CDbl(bitmax) For i As Integer = 0 To value.Length - 1 x(i) = CShort(Min(Max(b1, s * (value(i) + offset)), b2)) Next ' increment pointer currentspectrum += 1 'wrap If circular Then currentspectrum = currentspectrum Mod naxis1 End Set End Property ''' Whether the number of spectra acquired equals or exceeds the number defined ''' when the instance was created. ''' ''' False if fewer acquired; True if equal or greater. ''' Once full, the instance should written to the file, closed, and not used further. ''' But the instance need not be full to be written and closed. Public ReadOnly Property isfull() As Boolean Get Return Not circular AndAlso currentspectrum = naxis1 End Get End Property ''' Sets and returns the number of spectra in the spectrogram. Dim naxis1var As Integer Public Property naxis1(Optional ByVal newnaxis2 As Integer = 0) As Integer Get Return naxis1var End Get Set(ByVal value As Integer) If newnaxis2 > 0 AndAlso naxis2 <> newnaxis2 Then ' new value of naxis2 ReDim data(value - 1) naxis2 = newnaxis2 For i As Integer = 0 To value - 1 ReDim data(i)(receivercount * naxis2 - 1) Next Else ' naxis2 is unchanged ReDim Preserve data(value - 1) For i As Integer = naxis1 To value - 1 ReDim data(i)(receivercount * naxis2 - 1) Next End If naxis1var = value If circular AndAlso currentspectrum >= naxis1 Then currentspectrum = 0 End Set End Property End Class ''' ''' Provides an internet server attached to a radio receiver that feeds spectral data to Radio Sky ''' Spectrograph (RSS). RSS is software that displays data from a receiver and ''' optionally exports that data to the internet. It can also view data files. ''' ''' ''' To create an RSSServer object use: ''' dim client as New RSSServer(port, bincount, FrequencyStep, FrequencyCenter, FrequencyOffset) ''' where the first argument port is the socket port number, ''' the second parameter bincount is the number of spectral channels that are being transmitted, ''' and the three frequency parameters determine which of the spectral channels are being transmitted. ''' At this point RSS is set to server mode if not already, at which time it connects to the client and receivers ''' the number and frequency range of the data it subsequently receives. ''' To actually transmit data to RSS, one calls ''' client.DataHandler(buf, True) ''' where buf is a buffer containing bincount (u)shorts containing the spectral ''' data, and two additional bytes, each of which contains the hex code 0xFE. ''' The values transmitted must be limited to either 2^10-1 or 2^12-1 depending on the receiver word size. ''' The connection remains open and a stream of spectra may be transmitted arbitrarily. ''' RSS will assume they are generated at a uniform rate. ''' The connection is closed either by calling ''' client.stopall ''' or when RSS resets the connection, after which in either case the object ''' must be disposed of. ''' Public Class RSSServer Inherits TcpListener Dim stream As NetworkStream Dim WithEvents t As New Timer Dim clientSocket As Socket Dim rectype As ReceiverType Private bitmin, bitmax As Single Private c As ColorRes = ColorRes.twelvebit ''' ''' Creates and instance of the class with parameters set to receiver settings. ''' ''' The sockets port number. It must be 8888. ''' The number of spectral channels to be sent to RSS ''' The spectral resolution of the receiver's spectral data. ''' The center frequency of the frequency span to be sent to RSS, in HZ. ''' An offset frequency that is non-zero when the receiver employs down conversion. ''' The receiver type corresponding the to ReceiverType enumeration. ''' Public Sub New(ByVal port As Integer, _ ByVal fCount As Integer, ByVal dfin As Double, ByVal fCent As Double, _ Optional ByVal fOffset As Double = 0, _ Optional ByVal rt As RSS.ReceiverType = ReceiverType.RTLDongle, _ Optional ByVal cx As ColorRes = ColorRes.twelvebit) MyBase.New(New IPEndPoint(IPAddress.Loopback, port)) Debug.Print(rt.ToString) df = dfin ExportedChannelCount = fCount freqcent = fCent FreqOffset = fOffset rectype = rt Me.c = cx Select Case c Case ColorRes.tenbit : bitmin = 0 : bitmax = 1023 Case ColorRes.twelvebit : bitmin = 0 : bitmax = 4095 Case ColorRes.bipolar : bitmin = -32768 : bitmax = 32767 End Select ' start the listener Debug.Print("rssserver.new " & rectype.ToString) Start() Debug.Print("rssserver.new listener started") ' start the pending-polling timer t.Interval = 1000 t.Start() End Sub ' Thread signal. Public Shared clientConnected As New System.Threading.ManualResetEvent(False) Sub listenertimerhandler(ByVal x As Object, ByVal y As System.EventArgs) Handles t.Tick If Pending() Then t.Stop() ' Set the event to nonsignaled state. clientConnected.Reset() BeginAcceptSocket(AddressOf BASHandler, Me) ' Wait until a connection is made and processed before continuing. clientConnected.WaitOne() End If End Sub ''' Indicates that the underlying socket is listening for connections. Public ReadOnly Property listening() As Boolean Get Return t.Enabled End Get End Property Sub BASHandler(ByVal u As IAsyncResult) 'EndAcceptSocket(u) ' Get the listener that handles the client request. Dim listener As TcpListener = CType(u.AsyncState, TcpListener) ' End the operation and display the received data on the 'console. clientSocket = listener.EndAcceptSocket(u) clientSocket.Blocking = False ' Process the connection here. (Add the client to a ' server table, read data, etc.) printf("Client connected completed") ' Signal the calling thread to continue. clientConnected.Set() ' send the intro string Dim b() As Byte = IntroByte() clientSocket.Send(IntroByte) End Sub ''' ''' Returns the intro string that is sent to the remote host when a connection is first established. ''' ''' The intro string that is sent to the remote RSS client upon connecting. ''' The intro string informs the remote RSS clent when first connecting of the receiver's ''' frequency center, span, offset, and number of channels. Public Function IntroString() As String Dim r As String = sprintf( _ "F %d|S %d|O %d|C %d|R %d|", _ CInt(freqcent), CInt(freqspan), CInt(FreqOffset), ExportedChannelCount, ReceiverCount _ ) Debug.Print(r) Return r End Function ''' ''' Returns the intro string as an array of bytes containing the string's ascii codes. ''' ''' In byte form, the intro string that is sent to the remote RSS client upon connecting. ''' The intro string informs the remote RSS clent of the receiver's ''' frequency center, span, offset, and number of channels. Function IntroByte() As Byte() Return Text.Encoding.ASCII.GetBytes(IntroString) End Function ''' ''' Data to be sent to RSS passes through this function. ''' ''' Data to be sent to the client. Receiver samples are interleaved, ''' and their are info.FreqChans of each. Public Sub DataHandler(ByVal data() As Short, Optional ByVal bigendian As Boolean = False) ' byte buffer for writing to the stream Dim datalen As Integer = ExportedChannelCount * ReceiverCount Dim buflen As Integer = datalen * 2 + 2 Static buf() As Byte ' so that it is not continually created and disposed If buf Is Nothing OrElse buf.Length <> buflen Then ReDim buf(buflen - 1) End If If data.Length <> datalen Then Throw (New ApplicationException(sprintf( _ "%s.DataHandler - array provided (%d) does not have the correct length (%d).", _ Me.GetType.Name, data.Length, datalen))) Else ' reverse order of spectral channels (RSS spec) Array.Reverse(data) ' copy to a byte array with termination code BlockCopy(data, 0, buf, 0, 2 * data.Length) ' byte ordering (endianess) If bigendian Then For i As Integer = 0 To buf.Length - 2 Step 2 ' swap bytes Dim temp As Byte = buf(i) buf(i) = buf(i + 1) buf(i + 1) = temp Next End If buf(buf.Length - 2) = 254 buf(buf.Length - 1) = 254 ' send to the client ' debug->exceptions: uncheck 'break when socketexception is thrown' clientSocket.Send(buf) End If End Sub Public WriteOnly Property spectrum(Optional ByVal offset As Double = 0, Optional ByVal scale As Double = 1, Optional ByVal bigendian As Boolean = False) As Double() Set(ByVal data() As Double) ' byte buffer for writing to the stream Static buf1() As Short ' for the short data Static buf2() As Byte ' for the raw byte buffer Static buf3() As Double ' dual-receiver interleaved spectra Dim datalen As Integer = ExportedChannelCount * ReceiverCount Dim buflen As Integer = datalen * 2 + 2 ' so these arrays are not continually created and disposed If buf1 Is Nothing OrElse buf1.Length <> datalen Then ReDim buf1(datalen - 1) ReDim buf2(buflen - 1) ReDim buf3(datalen - 1) ' add termination bytes to the output byte buffer Array.Copy(RSSBase.fefe, 0, buf2, buf2.Length - 2, 2) printf("RSS.RSSServer.spectrum - chan count %d; rcvrs x cc %d; byte len %d", ExportedChannelCount, datalen, buflen) End If ' if dual receiver If ReceiverCount = 2 Then ' test ramp 'Static ramp1(), ramp2() As Double 'If ramp1 Is Nothing Then 'ReDim ramp1(datalen \ 2 - 1) 'ReDim ramp2(datalen \ 2 - 1) 'For i As Integer = 0 To datalen \ 2 - 1 'ramp1(i) = i / 1000 'ramp2(i) = 1.5 * ramp1(i) 'Next 'End If 'Array.Copy(ramp1, 0, buf3, 0, datalen \ 2) 'Array.Copy(ramp2, 0, buf3, datalen \ 2, datalen \ 2) ' reorder the array to interleaved 'RSSBase.interleave(data, buf3) 'data = buf3 End If ' data-length mismatch If data.Length <> datalen Then Throw (New ArgumentException(sprintf( _ "%s.DataHandler - array provided (%d) does not have the correct length (%d).", _ Me.GetType.Name, data.Length, datalen))) End If ' scale and clip Dim s As Double = scale * bitmax For i As Integer = 0 To datalen - 1 buf1(i) = CShort(Max(bitmin, Min(s * (data(i) + offset), bitmax))) Next ' reverse order of spectral channels (RSS spec) Array.Reverse(buf1) ' copy to the byte array, leaving termination code intact BlockCopy(buf1, 0, buf2, 0, buf2.Length - 2) ' switch byte ordering (endianess), if necessary If bigendian Then For i As Integer = 0 To buf2.Length - 3 Step 2 ' swap bytes Dim temp As Byte = buf2(i) Dim i1 As Integer = i + 1 buf2(i) = buf2(i1) buf2(i1) = temp Next End If ' send to the client ' debug->exceptions: uncheck 'break when socketexception is thrown' clientSocket.Send(buf2) End Set End Property ''' Float data to be sent to RSS passes through this function. ''' Data to be sent to the client. Receiver samples are interleaved, ''' and their are info.FreqChans of each. Public Sub datahandler(ByVal d() As Single, ByRef offset As Single, ByRef scale As Single, Optional ByVal bigendian As Boolean = False) Static dx() As Short If dx Is Nothing Then ReDim dx(d.Length - 1) For i As Integer = 0 To d.Length - 1 dx(i) = CShort(Min(Max(bitmin, (d(i) + offset) * scale), bitmax)) Next DataHandler(dx, bigendian) End Sub ''' Closes the socket and stops listening. Public Shadows Sub stopall() t.Stop() Sleep(t.Interval) DoEvents() If clientSocket IsNot Nothing Then clientSocket.Close() MyBase.Stop() End Sub ''' The number of Fourier channels that are emitted by the RSS server. ''' This property, and freqcent, are used to set the exact Fourier channels that ''' are emitted. ''' The Fourier-channel frequency bins are set by the sample rate fs and N. ''' So when setting a parameter, such as freqcent, to a value, that value is adjusted ''' so that it aligns with a frequency bin. Thus the return value of freqcent is in general not the same ''' as its set value. Public ExportedChannelCount As Integer ''' Center frequency of the span exported to the server. ''' Note that the value returned is slightly different from the value set. Public Property freqcent() As Double Get Return 0.5 * (freq1 + freq2) End Get Set(ByVal value As Double) chan1var = CInt(value / df - 0.5 * (ExportedChannelCount - 1)) chan2var = chan1var + ExportedChannelCount - 1 End Set End Property Public chan1var As Integer ''' Unliased Fourier channel number of the first channel exported by the server. Public ReadOnly Property chan1() As Integer Get Return chan1var End Get End Property Public chan2var As Integer ''' Unliased Fourier channel number of the last channel exported by the server. Public ReadOnly Property chan2() As Integer Get Return chan2var End Get End Property ''' The span of frequencies emitted by the server. ''' This is the frequency difference between the last and first frequency ''' bin centers. The true span is one bin larger. Public ReadOnly Property freqspan() As Double Get Return freq2 - freq1 End Get End Property ''' Unliased center frequency of the first Fourier channel exported by the server. Public ReadOnly Property freq1() As Double Get Return chan1var * df End Get End Property ''' >Unliased center frequency of the last Fourier channel exported by the server. Public ReadOnly Property freq2() As Double Get Return chan2var * df End Get End Property Private fovar As Double Public Property FreqOffset() As Double Get Return fovar End Get Set(ByVal value As Double) fovar = value End Set End Property ''' The number of receivers to be streamed. ''' Only one type is supported at the moment. Public ReadOnly Property ReceiverCount() As Integer Get Select Case rectype Case ReceiverType.DPS Case ReceiverType.FSX10bit Case ReceiverType.FSX12bit Case ReceiverType.FSX12bitSwitching Return 2 Case ReceiverType.FSX200 Case ReceiverType.ICOMTuneMode Case ReceiverType.RadioComPort Case ReceiverType.RTLDongle Return 1 Case ReceiverType.RTLWideSpectrum Case ReceiverType.SDR14 Case ReceiverType.SingleReceiver Return 1 Case ReceiverType.DualReceiver Return 2 End Select End Get End Property Public df As Double ''' Waits first for listening == true, then waits for listening == false. ''' An abort flag reference. The function exits when it goes high. ''' The interval with which to poll, in ms. Public Sub WaitForConnection(ByRef quit As Boolean, Optional ByVal intervalms As Integer = 300) ' wait for listening Do Until listening OrElse quit : Sleep(intervalms) : DoEvents() : Loop ' wait for not listening Do Until Not listening OrElse quit : Sleep(intervalms) : DoEvents() : Loop End Sub End Class ''' RSS receiver types. ''' Not all are supported. Public Enum ReceiverType ICOMTuneMode RadioComPort FSX200 FSX10bit FSX12bit FSX12bitSwitching SDR14 RTLDongle RTLWideSpectrum DPS SDRPlay SingleReceiver DualReceiver End Enum ''' Represents the choice of number of bits in the spectral data, right justified in a sixteen-bit word. Public Enum ColorRes ''' Sixteen-bit data. bipolar = 0 ''' Ten-bit data. tenbit = 4 ''' Twelve-bit data. twelvebit = 1 End Enum ''' Reads .sps files. ''' This class is in only a skeleton state. Public Class SPSReader Inherits rssbase ''' The time of the first spectrum. Public dateobs As DateTime ''' The time of the last spectrum. Public dateend As DateTime ''' The observatory's name. Public observatory As String ''' The observatory's location. Public location As String ''' Observatory latitude. Public latitude As Double ''' Observatory longitude Public longitude As Double ''' Observatory elevation (not used). Public elevation As Double ''' Time zone relative to GMT of the observatory. Public timezone As Short ''' Observer's name. Public observer As String ''' File author's name (not used). Public author As String ''' The number of receivers interleaved in the spectra. Public receivercount As Integer ''' The number of spectra in the spectrogram contained in the file. ''' This number must match the number of spectra actually written to the ''' file using the write methods. Public naxis1 As Integer ' count of spectra ''' The number of spectral channels in each spectrum. Public naxis2 As Short ' count of channels ''' The center frequency of each spectrum Public freqcent As Double ' center frequency of the spectra ''' The span of frequencies of each spectrum. Public freqspan As Double ' frequency span of the spectra ''' The number of bits of data in each spectral channel. Private bitmax As Short Private bitmin As Short Private currentspectrum As Integer = 0 Friend fid As FileStream Friend binReader As BinaryReader Public Sub New(ByVal filename As String) fid = New FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read) binReader = New BinaryReader(fid) With binReader Dim Version As String = .ReadChars(10) Dim freqstart As Double = .ReadDouble Dim freqend As Double = .ReadDouble freqcent = 0.5 * (freqstart + freqend) freqspan = freqend - freqstart latitude = .ReadDouble longitude = .ReadDouble Dim fill1 As Double = .ReadDouble fill1 = .ReadDouble timezone = .ReadInt16 Dim fill2 As String = .ReadChars(10) author = .ReadChars(20) : author = author.Trim observatory = .ReadChars(20) : observatory = observatory.Trim location = .ReadChars(40) : location = location.Trim naxis1 = CInt(.ReadInt16) Dim notelength As Integer = .ReadInt32 Dim note As String = System.Text.Encoding.ASCII.GetString(.ReadBytes(notelength)) note = note.Substring(4, note.Length - 9) Dim ele() As String = note.Split(Chr(255), "?"c) naxis1 = CInt(ele(0).Substring(6)) Dim freqlow As Double = CDbl(ele(1).Substring(4)) Dim freqhi As Double = CDbl(ele(2).Substring(3)) freqcent = 0.5 * (freqlow + freqhi) freqspan = freqhi - freqlow naxis2 = CInt(ele(3).Substring(5)) If ele(5).Length > 4 Then receivercount = CInt(ele(5).Substring(4)) Else receivercount = 1 c = CInt(ele(6).Substring(8)) End With End Sub Public ReadOnly Property spectrum(Optional ByVal data() As Short = Nothing) As Short() Get If currentspectrum < naxis1 Then Dim d() As Short If data Is Nothing Then ReDim d(receivercount * naxis2 - 1) Else d = data End If For i As Integer = 0 To d.Length - 1 d(i) = binReader.ReadInt16 Next Static buf(2 * receivercount * naxis2 - 1) As Byte BlockCopy(d, 0, buf, 0, 2 * receivercount * naxis2) Array.Reverse(buf) BlockCopy(buf, 0, d, 0, 2 * receivercount * naxis2) currentspectrum += 1 Return d Else Throw (New ArgumentException(sprintf("%s.spectrum - ran out of spectra.", Me.GetType.Name))) End If End Get End Property Public Sub close() fid.Flush() fid.Close() binReader.Close() End Sub End Class ''' Merges .sps files. ''' Untested. Public Class SPSMerge ''' Recipient of the data from the .sps files to be merged. Dim writer As SPSWriter Public Sub New(ByVal ParamArray filenames() As String) If UBound(filenames) < 0 Then ' get a list of file names Dim d As New OpenFileDialog With d d.CheckFileExists = True d.Filter = "RSS files|.sps" d.Title = "Load configuration" d.Multiselect = True d.ShowDialog() filenames = d.FileNames End With End If ' read the files and append the spectra to data Dim reader As SPSReader = Nothing For Each filename As String In filenames reader = New SPSReader(filename) With reader If writer Is Nothing Then ' create an spswriter for the reader's data writer = New SPSWriter(.naxis1, .naxis2, False, .c, .receivercount) writer.dateobs = reader.dateobs Else ' expand the writer's data to hold the new reader's data writer.naxis1 += .naxis1 End If Do Until writer.isfull ' copy the spectra from the file to the merge writer.spectrum = .spectrum Loop reader.close() End With Next ' copy last reader's metadata to the writer (excepting dateobs) With writer .dateend = reader.dateend .author = reader.author .observer = reader.observer .circular = False .freqcent = reader.freqcent .freqspan = reader.freqspan .observatory = reader.observatory .latitude = reader.latitude .location = reader.location .longitude = reader.longitude .elevation = reader.elevation .receivercount = reader.receivercount .timezone = reader.timezone End With End Sub Public Sub subtractbackground(ByVal limits() As Integer) With writer Dim n As Integer = limits(1) - limits(2) + 1 ' sum the background Dim background(.naxis2 - 1) As Single For j As Integer = 0 To .naxis2 - 1 Dim s As Single = 0 For i As Integer = 0 To n - 1 s += CSng(.data(i)(j)) Next background(j) = s Next ' compute the mean and convert to short Dim sn As Single = 1 / CSng(n) Dim bg(.naxis2 - 1) As Short For i As Integer = 0 To .naxis2 bg(i) = CShort(background(i) * sn) Next ' subtract the background from the entire array For j As Integer = 0 To .naxis1 - 1 Dim s As Single = 0 Dim a() As Short = .data(j) For i As Integer = 0 To .naxis2 - 1 a(i) = Min(-32768, Min(a(i) - bg(i), 32767)) Next Next End With End Sub Public Sub frequencysegment(Optional ByVal decimation As Integer = 1, Optional ByVal clip As Integer = 1000000000) ' decimate in frequency With writer For i As Integer = 0 To .naxis1 - 1 If decimation > 1 Then Dim s() As Short = .data(i) For j As Integer = 0 To .naxis2 - decimation Step decimation Dim a As Integer = 0 For k As Integer = j To j + decimation - 1 a += s(k) Next s(j \ decimation) = CShort(a / decimation) Next End If ' shorten each spectrum to the correct size Dim uab As Integer = Min(clip, .naxis2 \ decimation) If uab < .naxis2 Then .naxis2 = uab ReDim Preserve .data(i)(.naxis2 - 1) End If Next End With End Sub Public Sub timesegment(ByVal startindex As Integer, Optional ByVal endindex As Integer = 1000000000, Optional ByVal decimation As Integer = 1) ' decimate in time With writer ' new start and end times .dateend = .dateobs.Add(New TimeSpan(CLng(.dateend.Subtract(.dateobs).Ticks * endindex / .naxis1))) .dateobs = .dateobs.Add(New TimeSpan(CLng(.dateend.Subtract(.dateobs).Ticks * startindex / .naxis1))) ' process change Dim idx As Integer = 0 For i As Integer = startindex To Min(endindex, .naxis1 - decimation) Step decimation Dim s() As Short = .data(i) If decimation > 1 Then For j As Integer = 0 To .naxis2 - 1 Dim a As Integer = 0 For k As Integer = i To i + decimation - 1 a += .data(k)(j) Next s(j) = CShort(a / decimation) Next End If .data(idx) = s idx += 1 ' shorten spectrogram to the correct decimated size If idx < .naxis1 Then .naxis1 = idx ReDim Preserve .data(.naxis1 - 1) End If Next End With End Sub Public Sub export(ByVal obs As ObservatoryData.observatory, Optional ByVal filename As String = Nothing, Optional ByVal path As String = Nothing) writer.export(obs, filename, path) End Sub End Class End Namespace ' Jim Sky's code for writing header of .sps files 'Function GetModifiedNotes() ' 'here is where we can add any parameters we need to to the tempnotesstring ' 'this tempnotestring begins with a freeform notes area, and was expanded to allow the addition of an unlimited number ' 'of fields between *[[* and *]]* markers. Each field begins with capitalized token +value string+ CHR(255) ' On Error Resume Next ' If RCVR = stFSXdual Or RCVR = stDualEmulator Then DualSpecFile = True ' Dim TNSx As String ' TNSx = TempNotesString + "*[[*" ' If FileLoggedUsingUT Then TNSx = TNSx + "Logged Using UT" + Chr(255) ' If SaveView Then ' TNSx = TNSx + "SWEEPS" + Trim(Str$((EndSave - StartSave) + 1)) + Chr(255) ' Else ' TNSx = TNSx + "SWEEPS" + Trim(Str$(NumSweeps)) + Chr(255) ' End If ' TNSx = TNSx + "LOWF" + Trim(Str$(LowF)) + Chr(255) ' TNSx = TNSx + "HIF" + Trim(Str$(HiF)) + Chr$(255) ' TNSx = TNSx + "STEPS" + Trim(Str$(NumChannels)) + Chr$(255) ' TNSx = TNSx + "DUALSPECFILE" + Trim(CStr(DualSpecFile)) + Chr$(255) ' TNSx = TNSx + "COLORRES" + Trim(CStr(ColorRes)) + Chr$(255) ' TNSx = TNSx + "RCVR" + Trim(Str$(RCVR)) + Chr$(255) ' TNSx = TNSx + "BANNER0" + BannerTextTemplate(0) + Chr$(255) ' TNSx = TNSx + "BANNER1" + BannerTextTemplate(1) + Chr$(255) ' TNSx = TNSx + "ANTENNATYPE" + AntennaType + Chr$(255) ' TNSx = TNSx + "ANTENNAAZIMUTH" + AntennaAzimuth + Chr$(255) ' TNSx = TNSx + "ANTENNAELEVATION" + AntennaElevation + Chr$(255) ' TNSx = TNSx + "ANTENNAPOLARIZATION0" + AntennaPolarization0 + Chr$(255) ' TNSx = TNSx + "ANTENNAPOLARIZATION1" + AntennaPolarization1 + Chr$(255) ' TNSx = TNSx + "COLORFILE" + GetFileNameOnly(ColorFile) + Chr$(255) ' TNSx = TNSx + "COLOROFFSET0" + Str$(FixedColorOffset) + Chr$(255) ' If DualSpecFile Then TNSx = TNSx + "COLOROFFSET1" + Str$(FixedColorOffset2) + Chr$(255) ' TNSx = TNSx + "COLORGAIN0" + Str$(ColorGain) + Chr$(255) ' If DualSpecFile Then TNSx = TNSx + "COLORGAIN1" + Str$(ColorGain2) + Chr$(255) ' If CorrectionFileLoaded Then ' If OpMode = Client Then ' TNSx = TNSx + "CORRECTIONFILENAME" + "Not Local" + Chr$(255) ' Else ' TNSx = TNSx + "CORRECTIONFILENAME" + CorrectionArrayName + Chr$(255) ' End If ' For I = 0 To UBound(CorrectionArray) ' TNSx = TNSx + "CAXF" + Str$(I) + "|" + Str$(CorrectionArrayFreq(I)) + Chr$(255) ' Next I ' For I = 0 To UBound(CorrectionArray) ' TNSx = TNSx + "CAX1" + Str$(I) + "|" + Str(CorrectionArray(I)) + Chr$(255) ' Next I ' If RCVR = stDualEmulator Or RCVR = stFSXdual Then ' For I = 0 To UBound(CorrectionArray) ' TNSx = TNSx + "CAX2" + Str$(I) + "|" + Str$(CorrectionArray2(I)) + Chr$(255) ' Next I ' End If ' End If ' For I = 1 To UBound(ClockMetaData) ' TNSx = TNSx + "CLOCKMSG" + Trim(Str(I)) + " " + ClockMetaData(I) + Chr(255) ' Next I ' TNSx = TNSx + "*]]*" ' GetModifiedNotes = TNSx 'End Function