Module: Meshtastic::Forwarder

Defined in:
lib/meshtastic/forwarder.rb

Overview

libcotshrink protobuf decoding. Detail is a schema-level hash, not CoT XML.

Defined Under Namespace

Classes: UnsupportedFormat

Constant Summary collapse

MAX_BYTES =
1_048_576
SOURCES =
%w[DTED0 DTED1 DTED2 DTED3 LIDAR PFI USER ??? GPS SRTM1 COT PRI CALC ESTIMATED RTK DGPS GPS_PPS].freeze
EXTENSIONS =
[
  [:how, 3, %w[h-e h-g-i-g-o m-g]],
  [:geopointsrc, 6, SOURCES], [:altsrc, 6, SOURCES],
  [:role, 5, ['Team Member', 'Team Lead', 'HQ', 'Sniper', 'Medic', 'Forward Observer', 'RTO', 'K9']],
  [:battery, 8, nil], [:readiness, 2, :boolean], [:labels_on, 2, :boolean],
  [:height_unit, 4, nil], [:ce_human_input, 2, :boolean], [:tog, 2, :boolean],
  [:route_planning_method, 2, %w[Infil Exfil]],
  [:route_method, 4, %w[Driving Walking Flying Swimming Watercraft]],
  [:route_type, 2, ['On Foot', 'Vehicle']], [:route_route_type, 2, %w[Primary Secondary]],
  [:route_order, 2, ['Ascending Check Points', 'Descending Check Points']], [:route_stroke, 8, nil]
].freeze

Class Method Summary collapse

Class Method Details

.authors ⇒ Object



72
73
74
# File 'lib/meshtastic/forwarder.rb', line 72

public_class_method def self.authors
  '0day Inc.; libcotshrink schema and format: paulmandal (MIT)'
end

.decode(opts = {}) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/meshtastic/forwarder.rb', line 27

public_class_method def self.decode(opts = {})
  bytes = opts[:payload].to_s.b
  limit = byte_limit(opts)
  raise ArgumentError, 'Forwarder input exceeds byte limit' if bytes.bytesize > limit

  compression = bytes.start_with?("\x1f\x8b".b) ? :gzip : :none
  bytes = gunzip(payload: bytes, max_bytes: limit) if compression == :gzip
  raise UnsupportedFormat, 'libcotshrink EXI requires a schema-less EXI grammar decoder; not implemented in Ruby' if bytes.start_with?('$EXI') || (bytes.getbyte(0) && (bytes.getbyte(0) & 0xC0) == 0x80)

  message = ForwarderProtobuf::CotEvent.decode(bytes)
  extensions = unpack_extensions(value: message.customBytesExt)
  event = unpack_event(message: message, start_of_year: opts[:start_of_year])
  event[:how] = extensions[:how]
  {
    format: :libcotshrink_protobuf, compression: compression,
    protobuf: message.to_h, event: event, extensions: extensions
  }
rescue Google::Protobuf::ParseError => e
  raise ArgumentError, "invalid libcotshrink protobuf: #{e.message}"
end

.decode_chunks(opts = {}) ⇒ Object

Stateless reassembly: callers group only chunks known to belong together.

Raises:

  • (ArgumentError)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/meshtastic/forwarder.rb', line 57

public_class_method def self.decode_chunks(opts = {})
  chunks = opts[:chunks]
  raise ArgumentError, 'Forwarder requires one to fifteen chunks' unless chunks.is_a?(Array) && (1..15).cover?(chunks.length)

  fragments = chunks.map { |payload| parse_fragment(payload: payload) }
  count = fragments.first[:count]
  raise ArgumentError, 'inconsistent Forwarder chunk counts' unless fragments.all? { |fragment| fragment[:count] == count }
  raise ArgumentError, 'duplicate Forwarder chunk index' unless fragments.map { |fragment| fragment[:index] }.uniq.length == fragments.length
  raise ArgumentError, 'missing Forwarder chunks' unless fragments.length == count
  raise ArgumentError, 'Forwarder reassembly exceeds byte limit' if fragments.sum { |fragment| fragment[:body].bytesize } > byte_limit(opts)

  bytes = fragments.sort_by { |fragment| fragment[:index] }.map { |fragment| fragment[:body] }.join.b
  decode_body(opts.merge(payload: bytes))
end

.decode_packet(opts = {}) ⇒ Object

Packet payload includes the upstream (index << 4 | count) byte.



49
50
51
52
53
54
# File 'lib/meshtastic/forwarder.rb', line 49

public_class_method def self.decode_packet(opts = {})
  fragment = parse_fragment(opts.merge({}))
  return fragment unless fragment[:count] == 1

  decode_body(opts.merge(payload: fragment[:body]))
end

.help ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/meshtastic/forwarder.rb', line 76

public_class_method def self.help
  puts "USAGE:
    # Decode reassembled libcotshrink event bytes.
    #{self}.decode(
      payload: 'required - reassembled protobuf or GZIP bytes without chunk header',
      max_bytes: 'optional - input and decompressed byte ceiling up to 1048576',
      start_of_year: 'optional - explicit Time epoch matching sender year and timezone; otherwise return offsets'
    )
    # Decode one header-bearing port 257 packet.
    #{self}.decode_packet(
      payload: 'required - one complete Meshtastic Forwarder port payload',
      max_bytes: 'optional - maximum input and decompressed bytes',
      start_of_year: 'optional - explicit Time epoch for sender timestamps'
    )
    # Reassemble an explicitly grouped complete message.
    #{self}.decode_chunks(
      chunks: 'required - array of header-bearing packets from one message and sender',
      max_bytes: 'optional - maximum reassembled and decompressed bytes',
      start_of_year: 'optional - explicit Time epoch for sender timestamps'
    )
    # List decoder authors and upstream attribution.
    #{self}.authors
  "
end