VITA-49 Decoding

I have a Flex SDR Ham Radio Transceiver. I have used Node-Red to access its API for several very useful applications. This one has me stumped.

I want to get the internal voltage from the radio. I subscribed to the meter message and it sends the meter VITA-49 packet t to Node-Red via a UDP port. That has gone OK.

My problem is the message packet is VITA-49 format and further in a specific protocol for metadata. As I understand it, VITA-49 is a standard used for SDR radios, but also contains metadata, such as my desired internal radio voltage.

Here is a screenshot of the data. I have expanded a few so you can see the ASCII conversion:

Here is the message protocol, from the Flex Radio API manual:

Can anyone point me to code to convert the VITA-49 packets?

Alan

I don't think VITA-49 is an open standard so it may be hard to find any expertise. The part you have pasted seems to say the data is in 32 bit chunks (16 bit identifier and 16 bits data) - but all the packets you have captured are 32 bytes so 8 x as large... so it's not at all clear how they are related.

I agree with how difficult it is to get the specifics on VITA-49. Lots of article on how great it is, but lacking on coding details.

Good news ahead, maybe??

I also posted this request on the Flex Radio Forum and a fellow Ham Radio / Programmer. (Mark) responded that he had written such a decoder using Swift. Here is a link to the Git Hub containing the code: VITA Decoder Code Using Swift.

Exchanging e-mails with Mark, he says he will provide easy to follow methodology of his code so that a knowledgable person (hoping one of you on this forum), could use his guidance to write the code for a Node-Red Function. It needs to be in JavaScript, right?

When I get Doug's information, I will post it on this forum.

Maybe a more knowledgeable Node-Red person can read the SWIFT code on the Git Hub site and consider the project.

I did see that the VITA standard is very popular with the Software Defined Radio community and has broad application for controlling radios, so perhaps that would entice you, programmers, out there to publish a convertor flow or node?? Hoping so.

Alan

Here is the VITA Decoder Code:

//
//  Vita.swift
//  CommonCode
//
//  Created by Douglas Adams on 5/9/17.
//  Copyright © 2018 Douglas Adams & Mario Illgen. All rights reserved.
//

import Foundation

// ------------------------------------------------------------------------------
// MARK: - VITA header struct implementation
//
//      provides decoding and encoding services for Vita encoding
//      see http://www.vita.com
//
// ------------------------------------------------------------------------------

public struct VitaHeader {
  
  // this struct mirrors the structure of a Vita Header
  //      some of these fields are optional in a generic Vita-49 header
  //      however they are always present in the Flex usage of Vita-49
  //
  //      all of the UInt16 & UInt32 fields must be BigEndian
  //
  //      This header is 28 bytes / 4 UInt32's
  //
  var packetDesc                            : UInt8 = 0
  var timeStampDesc                         : UInt8 = 0                           // the lsb four bits are used for sequence number
  var packetSize                            : UInt16 = 0
  var streamId                              : UInt32 = 0
  var oui                                   : UInt32 = 0
  var classCodes                            : UInt32 = 0
  var integerTimeStamp                      : UInt32 = 0
  var fractionalTimeStampMsb                : UInt32 = 0
  var fractionalTimeStampLsb                : UInt32 = 0
}

// ------------------------------------------------------------------------------
// MARK: - VITA class implementation
// ------------------------------------------------------------------------------

public class Vita {
  
  // this struct includes, in a more readily inspectable form, all of the properties
  // needed to populate a Vita Data packet. The "encode" instance method converts this
  // struct into a Vita Data packet. The "decode" static method converts a supplied
  // Vita Data packet into a Vita struct.
  
  static let DiscoveryStreamId              : UInt32 = 0x00000800

  // ----------------------------------------------------------------------------
  // MARK: - Internal properties
  
  // filled with defaults, values are changed when created
  //      Types are shown for clarity
  
  var packetType                            : PacketType = .extDataWithStream     // Packet type
  var classCode                             : PacketClassCode = .panadapter       // Packet class code
  var streamId                              : UInt32 = 0                          // Stream ID
  
  var classIdPresent                        : Bool = true                         // Class ID present
  var trailerPresent                        : Bool = false                        // Trailer present
  var tsiType                               : TsiType = .utc                      // Integer timestamp type
  var tsfType                               : TsfType = .sampleCount              // Fractional timestamp type
  var sequence                              : Int = 0                             // Mod 16 packet sequence number
  var packetSize                            : Int = 0                             // Size of packet (32 bit chunks)
  var integerTimestamp                      : UInt32 = 0                          // Integer portion
  var fracTimeStampMsb                      : UInt32 = 0                          // fractional portion - MSB 32 bits
  var fracTimeStampLsb                      : UInt32 = 0                          // fractional portion -LSB 32 bits
  var oui                                   : UInt32 = kFlexOui                   // Flex Radio oui
  var informationClassCode                  : UInt32 = kFlexInformationClassCode  // Flex Radio classCode
  var payloadData                           = [UInt8]()                           // Array of bytes in payload
  var payloadSize                           : Int = 0                             // Size of payload (bytes)
  var trailer                               : UInt32 = 0                          // Trailer, 4 bytes (if used)
  var headerSize                            : Int = MemoryLayout<VitaHeader>.size // Header size (bytes)
  
  // ----------------------------------------------------------------------------
  // MARK: - Class methods

//  /// Create a Data type containing a Vita Discovery stream
//  ///
//  /// - Parameter payload:        the Discovery payload (as an array of String)
//  /// - Returns:                  a Data type containing a Vita Discovery stream
//  ///
//  public class func discovery(payload: [String]) -> Data? {
//
//    // create a new Vita class (w/defaults & extDataWithStream / Discovery)
//    let vita = Vita(type: .discovery, streamId: Vita.DiscoveryStreamId)
//    
//    // concatenate the strings, separated by space
//    let payloadString = payload.joined(separator: " ")
//    
//    // calculate the actual length of the payload (in bytes)
//    vita.payloadSize = payloadString.lengthOfBytes(using: .ascii)
//    
//    //        // calculate the number of UInt32 that can contain the payload bytes
//    //        let payloadWords = Int((Float(vita.payloadSize) / Float(MemoryLayout<UInt32>.size)).rounded(.awayFromZero))
//    //        let payloadBytes = payloadWords * MemoryLayout<UInt32>.size
//    
//    // create the payload array at the appropriate size (always a multiple of UInt32 size)
//    var payloadArray = [UInt8](repeating: 0x20, count: vita.payloadSize)
//    
//    // packet size is Header + Payload (no Trailer)
//    vita.packetSize = vita.payloadSize + MemoryLayout<VitaHeader>.size
//    
//    // convert the payload to an array of UInt8
//    let cString = payloadString.cString(using: .ascii)!
//    for i in 0..<cString.count - 1 {
//      payloadArray[i] = UInt8(cString[i])
//    }
//    // give the Vita struct a pointer to the payload
//    vita.payloadData = payloadArray
//    
//    // return the encoded Vita class as Data
//    return Vita.encodeAsData(vita)
//  }
  /// Decode a Data type into a Vita class
  ///
  /// - Parameter data:         a Data type containing a Vita stream
  /// - Returns:                a Vita class
  ///
  public class func decodeFrom(data: Data) -> Vita? {
    
    let kVitaMinimumBytes                   = 28                                  // Minimum size of a Vita packet (bytes)
    let kPacketTypeMask                     : UInt8 = 0xf0                        // Bit masks
    let kClassIdPresentMask                 : UInt8 = 0x08
    let kTrailerPresentMask                 : UInt8 = 0x04
    let kTsiTypeMask                        : UInt8 = 0xc0
    let kTsfTypeMask                        : UInt8 = 0x30
    let kPacketSequenceMask                 : UInt8 = 0x0f
    let kInformationClassCodeMask           : UInt32 = 0xffff0000
    let kPacketClassCodeMask                : UInt32 = 0x0000ffff
    let kOffsetOptionals                    = 4                                   // byte offset to optional header section
    let kTrailerSize                        = 4                                   // Size of a trailer (bytes)
    
    var headerCount = 0
    
    let vita = Vita()
    
    // packet too short - return
    if data.count < kVitaMinimumBytes { return nil }
    
    // map the packet to the VitaHeader struct
    let vitaHeader = (data as NSData).bytes.bindMemory(to: VitaHeader.self, capacity: 1)
    
    // capture Packet Type
    guard let pt = PacketType(rawValue: (vitaHeader.pointee.packetDesc & kPacketTypeMask) >> 4) else {
      return nil
    }
    vita.packetType = pt
    
    // capture ClassId & TrailerId present
    vita.classIdPresent = (vitaHeader.pointee.packetDesc & kClassIdPresentMask) == kClassIdPresentMask
    vita.trailerPresent = (vitaHeader.pointee.packetDesc & kTrailerPresentMask) == kTrailerPresentMask
    
    // capture Time Stamp Integer
    guard let intStamp = TsiType(rawValue: (vitaHeader.pointee.timeStampDesc & kTsiTypeMask) >> 6) else {
      return nil
    }
    vita.tsiType = intStamp
    
    // capture Time Stamp Fractional
    guard let fracStamp = TsfType(rawValue: (vitaHeader.pointee.timeStampDesc & kTsfTypeMask) >> 4) else {
      return nil
    }
    vita.tsfType = fracStamp
    
    // capture PacketCount & PacketSize
    vita.sequence = Int((vitaHeader.pointee.timeStampDesc & kPacketSequenceMask))
    vita.packetSize = Int(CFSwapInt16BigToHost(vitaHeader.pointee.packetSize)) * 4
    
    // create an UnsafePointer<UInt32> to the optional words of the packet
    let vitaOptionals = (data as NSData).bytes.advanced(by: kOffsetOptionals).bindMemory(to: UInt32.self, capacity: 6)
    
    // capture Stream Id (if any)
    if vita.packetType == .ifDataWithStream || vita.packetType == .extDataWithStream {
      vita.streamId = CFSwapInt32BigToHost(vitaOptionals.pointee)
      
      // Increment past this item
      headerCount += 1
    }
    
    // capture Oui, InformationClass code & PacketClass code (if any)
    if vita.classIdPresent == true {
      vita.oui = CFSwapInt32BigToHost(vitaOptionals.advanced(by: headerCount).pointee) & kOuiMask
      
      let value = CFSwapInt32BigToHost(vitaOptionals.advanced(by: headerCount + 1).pointee)
      vita.informationClassCode = (value & kInformationClassCodeMask) >> 16
      
      guard let cc = PacketClassCode(rawValue: UInt16(value & kPacketClassCodeMask)) else {
        return nil
      }
      vita.classCode = cc
      
      // Increment past these items
      headerCount += 2
    }
    
    // capture the Integer Time Stamp (if any)
    if vita.tsiType != .none {
      // Integer Time Stamp present
      vita.integerTimestamp = CFSwapInt32BigToHost(vitaOptionals.advanced(by: headerCount).pointee)
      
      // Increment past this item
      headerCount += 1
    }
    
    // capture the Fractional Time Stamp (if any)
    if vita.tsfType != .none {
      // Fractional Time Stamp present
      vita.fracTimeStampMsb = CFSwapInt32BigToHost(vitaOptionals.advanced(by: headerCount).pointee)
      vita.fracTimeStampLsb = CFSwapInt32BigToHost(vitaOptionals.advanced(by: headerCount + 1).pointee)
      
      // Increment past these items
      headerCount += 2
    }
    
    // calculate the Header size (bytes)
    vita.headerSize = ( 4 * (headerCount + 1) )
    // calculate the payload size (bytes)
    // NOTE: The data payload size is NOT necessarily a multiple of 4 bytes (it can be any number of bytes)
    vita.payloadSize = data.count - vita.headerSize - (vita.trailerPresent ? kTrailerSize : 0)
    
    // initialize the payload array & copy the payload data into it
    vita.payloadData = [UInt8](repeating: 0x00, count: vita.payloadSize)
    (data as NSData).getBytes(&vita.payloadData, range: NSMakeRange(vita.headerSize, vita.payloadSize))
    
    // capture the Trailer (if any)
    if vita.trailerPresent {
      // calculate the pointer to the Trailer (must be the last 4 bytes of the packet)
      let vitaTrailer = (data as NSData).bytes.advanced(by: data.count - 4).bindMemory(to: UInt32.self, capacity: 1)
      
      // capture the Trailer
      vita.trailer = CFSwapInt32BigToHost(vitaTrailer.pointee)
    }
    return vita
  }
  /// Encode a Vita class as a Data type
  ///
  /// - Returns:          a Data type containing the Vita stream
  ///
  public class func encodeAsData(_ vita: Vita) -> Data? {
    
    // TODO: Handle optional fields
    
    // create a Header struct
    var header = VitaHeader()
    
    // populate the header fields from the Vita struct
    
    // packet type
    header.packetDesc = (vita.packetType.rawValue & 0x0f) << 4
    
    // class id & trailer flags
    if vita.classIdPresent { header.packetDesc |= Vita.kClassIdPresentMask }
    if vita.trailerPresent { header.packetDesc |= Vita.kTrailerPresentMask }
    
    // time stamps
    header.timeStampDesc = ((vita.tsiType.rawValue & 0x03) << 6) | ((vita.tsfType.rawValue & 0x03) << 4)
    
    header.integerTimeStamp = CFSwapInt32HostToBig(vita.integerTimestamp)
    header.fractionalTimeStampLsb = CFSwapInt32HostToBig(vita.fracTimeStampLsb)
    header.fractionalTimeStampMsb = CFSwapInt32HostToBig(vita.fracTimeStampMsb)
    
    // sequence number
    header.timeStampDesc |= (UInt8(vita.sequence) & 0x0f)
    
    // oui
    header.oui = CFSwapInt32HostToBig(Vita.kFlexOui & Vita.kOuiMask)
    
    // class codes
    let classCodes = UInt32(vita.informationClassCode << 16) | UInt32(vita.classCode.rawValue)
    header.classCodes = CFSwapInt32HostToBig(classCodes)
    
    // packet size (round up to allow for OpusTx with payload bytes not a multiple of 4)
    let adjustedPacketSize = UInt16( (Float(vita.packetSize) / 4.0).rounded(.up))
    header.packetSize = CFSwapInt16HostToBig( adjustedPacketSize )
    
    // stream id
    header.streamId = CFSwapInt32HostToBig(vita.streamId)
    
    // create the Data type and populate it with the VitaHeader
    var data = Data(bytes: &header, count: MemoryLayout<VitaHeader>.size)
    
    // append the payload bytes
    data.append(&vita.payloadData, count: vita.payloadSize)
    
    // is there a Trailer?
    if vita.trailerPresent {
      
      // YES, append the trailer bytes
      data.append( Data(bytes: &vita.trailer, count: MemoryLayout<UInt32>.size) )
    }
    
    // return the Data type
    return data
  }
  /// Parse a Vita class containing a Discovery broadcast
  ///
  /// - Returns:        a RadioParameters struct (or nil)
  ///
  public class func parseDiscovery(_ vita: Vita) -> RadioParameters? {
    
    // is this a Discovery packet?
    if vita.classIdPresent && vita.classCode == .discovery {
      
      // YES, create a minimal RadioParameters with now as "lastSeen"
      let radio = RadioParameters()
      
      // Payload is a series of strings of the form <key=value> separated by ' ' (space)
      let payloadData = NSString(bytes: vita.payloadData, length: vita.payloadSize, encoding: String.Encoding.ascii.rawValue)! as String

      // parse into a KeyValuesArray
      let keyValues = payloadData.keyValuesArray()
      
      // process each key/value pair, <key=value>
      for kv in keyValues {
        
        // check for unknown keys
        guard let token = DiscoveryToken(rawValue: kv.key) else {
          
          // unknown Key, log it and ignore the Key
          Api.sharedInstance.log.msg("Unknown token - \(kv.key)", level: .warning, function: #function, file: #file, line: #line)
          continue
        }
        
        switch token {
          
        case .callsign:
          radio.callsign = kv.value
          
        case .fpcMac:
          radio.fpcMac = kv.value
          
        case .inUseHost:
          radio.inUseHost = kv.value
          
        case .inUseIp:
          radio.inUseIp = kv.value
          
        case .ipAddress:
          radio.ipAddress = kv.value
          
        case .maxLicensedVersion:
          radio.maxLicensedVersion = kv.value
          
        case .model:
          radio.model = kv.value
          
        case .name:
          radio.name = kv.value
          
        case .nickname:
          radio.nickname = kv.value
          
        case .port:
          radio.port = kv.value.iValue()
          
        case .protocolVersion:
          radio.protocolVersion = kv.value
          
        case .radioLicenseId:
          radio.radioLicenseId = kv.value
          
        case .requiresAdditionalLicense:
          radio.requiresAdditionalLicense = kv.value
          
        case .serialNumber:
          radio.serialNumber = kv.value
          
        case .status:
          radio.status = kv.value
          
        case .firmwareVersion:
          radio.firmwareVersion = kv.value
          
        // satisfy the switch statement, not a real token
        case .lastSeen:
          break
        }
      }
      // is it a valid Discovery packet?
      if radio.ipAddress != "" && radio.port != 0 && radio.model != "" && radio.serialNumber != "" {
        // YES
        return radio
      }
    }
    // Not a Discovery packet
    return nil
  }
  
  // ----------------------------------------------------------------------------
  // MARK: - Initialization
  
  /// Initialize Vita struct with the defaults above
  ///
  init() {
    // nothing needed, all values are defaulted
  }
  /// Initialize Vita with specific settings
  ///
  /// - Parameters:
  ///   - type:           the type of Vita
  ///   - streamId:       a StreamId
  ///
  convenience init(type: VitaType, streamId: UInt32) {
    
    switch type {
    case .opusTx:
      self.init(packetType: .extDataWithStream, classCode: .daxAudio, streamId: streamId, tsi: .other, tsf: .sampleCount)
      
    case .txAudio:
      self.init(packetType: .ifDataWithStream, classCode: .daxAudio, streamId: streamId, tsi: .other, tsf: .sampleCount)
    }
  }
  /// Initialize a Vita struct as a dataWithStream (Ext or If)
  ///
  /// - Parameters:
  ///   - packetType:     a Vita Packet Type (.extDataWithStream || .ifDataWithStream)
  ///   - classCode:      a Vita Class Code
  ///   - streamId:       a Stream ID (as a String, no "0x")
  ///   - tsi:            the type of Integer Time Stamp
  ///   - tsf:            the type of Fractional Time Stamp
  /// - Returns:          a partially populated Vita struct
  ///
  init(packetType: PacketType, classCode: PacketClassCode, streamId: UInt32, tsi: TsiType, tsf: TsfType) {
    
    assert(packetType == .extDataWithStream || packetType == .ifDataWithStream)
    
    self.packetType = packetType
    self.classCode = classCode
    self.streamId = streamId
    self.tsiType = tsi
    self.tsfType = tsf
    
    // default values for:  HeaderSize, Oui, InformationClassCode, TimeStamp(s), Sequence,
    //                      PacketSize, Payload, PayloadSize, Trailer
    
    // to be changed later: TimeStamps, Sequence, PacketSize, Payload, PayloadSize, Trailer
  }

  // ----------------------------------------------------------------------------
  // MARK: - Instance methods
  
  /// Return a String description of a Vita class
  ///
  /// - Returns:          a String describing the Vita class
  ///
  public func desc() -> String {

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .none
    dateFormatter.timeStyle = .medium

    let date = Date(timeIntervalSinceReferenceDate: Double(integerTimestamp) )

    var payloadString = ""
    for i in 1...payloadSize {
      payloadString += String(format: "%02X", payloadData[i-1]) + " "
      if (i % 8) == 0 { payloadString += "  " }
      if (i % 16) == 0 { payloadString += "\n" }
    }

    let adjustedPacketSize = Int( (Float(packetSize) / 4.0).rounded(.up))
    let warning = ( (headerSize / 4) + (payloadSize / 4) !=  adjustedPacketSize ? "WARNING: **** Payload size (bytes) not a multiple of 4 ****" : "")
    
    let timeStamp = (tsiType == .utc ? dateFormatter.string(from: date) : String(format: "%d", integerTimestamp))
    
    return
      "packetType           = \(packetType.description())\n" +
      "classIdPresent       = \(classIdPresent)\n" +
      "trailerPresent       = \(trailerPresent)\n" +
      "tsi type             = \(tsiType.description())\n" +
      "tsf type             = \(tsfType.description())\n" +
      "sequence             = \(sequence)\n" +
      "streamId             = \(streamId.hex)\n" +
      "oui                  = \(oui == Vita.kFlexOui ? "Flex Radio" : "Unknown")\n" +
      "informationClassCode = \(informationClassCode == Vita.kFlexInformationClassCode ? "Flex Radio" : "Unknown")\n" +
      "classCode            = \(classCode.description())\n" +
      "integerTimeStamp     = \(timeStamp)\n" +
      "fracTimeStampMsb     = \(fracTimeStampMsb)\n" +
      "fracTimeStampLsb     = \(fracTimeStampLsb)\n" +
      "trailer              = \(trailerPresent ? trailer.hex : "None")\n" +
      "payload:\n\(payloadString)\n" +
      "----------------------------------------------\n" +
      "headerSize           = \(headerSize) (bytes),  \(headerSize / 4) (UInt32)\n" +
      "payloadSize          = \(payloadSize) (bytes), \(payloadSize / 4) (UInt32)\n" +
      "packetSize           = \(packetSize) (bytes), \(adjustedPacketSize) (UInt32)\n" +
      "\n\(warning)\n\n"
  }
}

// --------------------------------------------------------------------------------
// MARK: - Vita Class extensions
//              - Static properties
//              - DiscoveryToken enum
//              - Vita Packet enums
// --------------------------------------------------------------------------------

extension Vita {
  
  // Flex specific codes
  static let kFlexOui                       : UInt32 = 0x1c2d
  static let kOuiMask                       : UInt32 = 0x00ffffff
  static let kFlexInformationClassCode      : UInt32 = 0x534c
  static let kClassIdPresentMask            : UInt8 = 0x08
  static let kTrailerPresentMask            : UInt8 = 0x04
  
  enum VitaType {
    case opusTx
    case txAudio
  }
  
  enum DiscoveryToken : String {            // Discovery Tokens
    case callsign
    case fpcMac                             = "fpc_mac"
    case inUseHost                          = "inuse_host"
    case inUseIp                            = "inuse_ip"
    case ipAddress                          = "ip"
    case maxLicensedVersion                 = "max_licensed_version"
    case model
    case name
    case nickname
    case port
    case protocolVersion                    = "discovery_protocol_version"
    case radioLicenseId                     = "radio_license_id"
    case requiresAdditionalLicense          = "requires_additional_license"
    case serialNumber                       = "serial"
    case status
    case firmwareVersion                    = "version"
    
    case lastSeen   // not a real token
  }
  public enum PacketType : UInt8 {          // Packet Types
    case ifData                             = 0x00
    case ifDataWithStream                   = 0x01
    case extData                            = 0x02
    case extDataWithStream                  = 0x03
    case ifContext                          = 0x04
    case extContext                         = 0x05
    
    func description() -> String {
      switch self {
      case .ifData:
        return "IfData"
      case .ifDataWithStream:
        return "IfDataWithStream"
      case .extData:
        return "ExtData"
      case .extDataWithStream:
        return "ExtDataWithStream"
      case .ifContext:
        return "IfContext"
      case .extContext:
        return "ExtContext"
      }
    }
  }
  public enum TsiType : UInt8 {             // Timestamp - Integer
    case none                               = 0x00
    case utc                                = 0x01
    case gps                                = 0x02
    case other                              = 0x03
    
    func description() -> String {
      switch self {
      case .none:
        return "None"
      case .utc:
        return "Utc"
      case .gps:
        return "Gps"
      case .other:
        return "Other"
      }
    }
  }
  public enum TsfType : UInt8 {             // Timestamp - Fractional
    case none                               = 0x00
    case sampleCount                        = 0x01
    case realtime                           = 0x02
    case freeRunning                        = 0x03
    
    func description() -> String {
      switch self {
      case .none:
        return "None"
      case .sampleCount:
        return "SampleCount"
      case .realtime:
        return "Realtime"
      case .freeRunning:
        return "FreeRunning"
      }
    }
  }
  public enum PacketClassCode : UInt16 {    // Packet Class Codes
    case meter                              = 0x8002
    case panadapter                         = 0x8003
    case waterfall                          = 0x8004
    case opus                               = 0x8005
    case daxIq24                            = 0x02e3
    case daxIq48                            = 0x02e4
    case daxIq96                            = 0x02e5
    case daxIq192                           = 0x02e6
    case daxAudio                           = 0x03e3
    case discovery                          = 0xffff
    
    func description() -> String {
      switch self {
      case .meter:
        return "Meter"
      case .panadapter:
        return "Panadapter"
      case .waterfall:
        return "Waterfall"
      case .opus:
        return "Opus"
      case .daxIq24:
        return "DaxIq24"
      case .daxIq48:
        return "DaxIq48"
      case .daxIq96:
        return "DaxIq96"
      case .daxIq192:
        return "DaxIq192"
      case .daxAudio:
        return "DaxAudio"
      case .discovery:
        return "Discovery"
      }
    }
  }
}

Well if you have an executable you could just call it from the exec node in Node-RED and grab the output.

Doug has replied on the decoding methodology.

Because I am only receiving the meter packets from the radio, if I understand correctly:

  1. Convert from Big-Endian to Little-Endian format,
  2. Parse out the first 28 bits,
  3. Test for Number of Meters
  4. Parse based on number of meters.

I searched for Big to Little Endian conversions and found a number of discussions on this. Could we start with the best way to do the conversion?

I reprinted Doug's response following this message. Also, I can send the pdf, which Doug provided, of the VITA49 Standards, via e-mail request.

Alan


Alan,

Assuming that the data you are receiving has all of the UDP packet stuff stripped from it, the first 28 bytes of the Vita packet are as follows:

packetDesc 1 byte
timeStampDesc 4 bits
sequenceNumber 4 bits
packetSize 2 bytes
streamId 4 bytes
oui 4 bytes
classCodes 4 bytes
integerTimeStamp 4 bytes
fractionalTimeStampMsb 4 bytes
fractionalTimeStampLsb 4 bytes

If you know that you have a Meter packet, you can basically ignore these. In my case where I’m receiving lots of other UDP streams, I need to check all of these to determine what type of packet I’ve received (e.g. panadapter, waterfall, audio, etc.). All of the multi-byte items are in Big-Endian form and may need to be converted to your host format (e.g. Little-Endian, the Mac and PC’s are Little-Endian internally).

The remainder of the Meter packet is the so-called “payload”, i.e. the actual Meter data. The payload is a string of bytes.

The payload of a Meter packet can contain one or more meters. You can determine the number of meters in the packet by dividing the payload size (in bytes) by 4. I.e. each meter takes up four bytes as follows:

meterNumber 2 bytes
meterValue 2 bytes

Again, these multi-byte values are in Big-Endian form and may need to be converted to your host format (e.g. Little-Endian). They are unsigned 16-bit numbers.

I hope all this helps. I’ve attached a copy of the Vita spec (it’s old but probably still correct for the things you need). Feel free to ask questions.

Good Luck.

73’s
Doug - K3TZR

Attachments area
Thanks, I'll check them out.This is great, thank you!Awesome, thanks!
Reply
Forward

Doug just sent me this addition:

"There should be one correction, the meter values are signed integers."

So basically we need the last 4 bytes in the 32byte packet... - can you copy paste any examples of a full packet in hex format so we can see the values ?