Class: Takagi::Message::Outbound

Inherits:
Base
  • Object
show all
Defined in:
lib/takagi/message/outbound.rb,
sig/takagi/message/outbound.rbs

Overview

Class for outbound message that is coming from server

Instance Attribute Summary

Attributes inherited from Base

#code, #message_id, #options, #payload, #token, #type, #version

Instance Method Summary collapse

Methods inherited from Base

#coap_code_to_method, #coap_method_to_code, #coerce_option_value, #decode_extended_value, #extract_payload, #parse, #parse_options, #store_option

Constructor Details

#initialize(code:, payload:, token: nil, message_id: nil, type: CoAP::Registries::MessageType::NON, options: {}, transport: :udp) ⇒ Outbound

Returns a new instance of Outbound.

Parameters:

  • code: (Object)
  • payload: (Object)
  • token: (Object, nil) (defaults to: nil)
  • message_id: (Object, nil) (defaults to: nil)
  • type: (Object) (defaults to: CoAP::Registries::MessageType::NON)
  • options: (::Hash[untyped, untyped]) (defaults to: {})


7
8
9
10
11
12
13
14
15
16
17
# File 'lib/takagi/message/outbound.rb', line 7

def initialize(code:, payload:, token: nil, message_id: nil, type: CoAP::Registries::MessageType::NON, options: {}, transport: :udp)
  super(nil, transport: transport)  # Call Base.initialize with transport
  @code = coap_method_to_code(code)
  @token = token || ''.b
  @message_id = message_id || rand(0..0xFFFF)
  @type = type
  @options = normalize_options(options)

  # Serialize payload using content-format from options
  @payload = serialize_payload(payload)
end

Instance Method Details

#build_headerObject

Returns:

  • (Object)


122
123
124
125
126
127
128
# File 'lib/takagi/message/outbound.rb', line 122

def build_header
  version = Takagi::CoAP::VERSION
  type = @type || CoAP::Registries::MessageType::ACK # Default ACK
  token_length = @token.bytesize
  version_type_token_length = (version << 6) | (type << 4) | token_length
  [version_type_token_length, @code, @message_id].pack('CCn')
end

#build_options_sectionObject

Returns:

  • (Object)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/takagi/message/outbound.rb', line 134

def build_options_section
  return ''.b if @options.empty?

  encoded = ''.b
  last_option_number = 0

  flattened_options.each do |number, value|
    value_bytes = encode_option_value(value)
    delta = number - last_option_number

    delta_nibble, delta_extension = encode_option_header_value(delta)
    length_nibble, length_extension = encode_option_header_value(value_bytes.bytesize)

    option_byte = (delta_nibble << 4) | length_nibble
    encoded << option_byte.chr
    encoded << delta_extension if delta_extension
    encoded << length_extension if length_extension
    encoded << value_bytes

    last_option_number = number
  end

  encoded
end

#build_payload_sectionObject

Returns:

  • (Object)


159
160
161
162
163
# File 'lib/takagi/message/outbound.rb', line 159

def build_payload_section
  return ''.b if @payload.nil? || @payload.empty?

  "\xFF".b + @payload.b
end

#encode_integer_option_value(value) ⇒ Object

Parameters:

  • value (Object)

Returns:

  • (Object)


198
199
200
201
202
203
204
205
206
207
# File 'lib/takagi/message/outbound.rb', line 198

def encode_integer_option_value(value)
  return ''.b if value.zero?

  bytes = []
  while value.positive?
    bytes << (value & 0xFF)
    value >>= 8
  end
  bytes.reverse.pack('C*')
end

#encode_option_header_value(value) ⇒ Object

Parameters:

  • value (Object)

Returns:

  • (Object)


209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/takagi/message/outbound.rb', line 209

def encode_option_header_value(value)
  case value
  when 0..12
    [value, nil]
  when 13..268
    [13, [value - 13].pack('C')]
  when 269..65_804
    [14, [value - 269].pack('n')]
  else
    raise ArgumentError, 'Option value too large'
  end
end

#encode_option_value(value) ⇒ Object

Parameters:

  • value (Object)

Returns:

  • (Object)


189
190
191
192
193
194
195
196
# File 'lib/takagi/message/outbound.rb', line 189

def encode_option_value(value)
  case value
  when Integer
    encode_integer_option_value(value)
  else
    value.to_s.b
  end
end

#flattened_optionsObject

Returns:

  • (Object)


183
184
185
186
187
# File 'lib/takagi/message/outbound.rb', line 183

def flattened_options
  @options.flat_map do |number, values|
    values.map { |value| [number, value] }
  end.sort_by.with_index { |(number, _), index| [number, index] }
end

#log_final_packet(packet) ⇒ Object

Parameters:

  • packet (Object)

Returns:

  • (Object)


165
166
167
# File 'lib/takagi/message/outbound.rb', line 165

def log_final_packet(packet)
  @logger.debug "Final CoAP packet: #{packet.inspect}"
end

#log_generationObject

Returns:

  • (Object)


96
97
98
99
# File 'lib/takagi/message/outbound.rb', line 96

def log_generation
  @logger.debug "Generating CoAP packet for code #{@code}, payload #{@payload.inspect}, " \
                "message_id #{@message_id}, token #{@token.inspect}, type #{@type}"
end

#normalize_options(options) ⇒ Object

Parameters:

  • options (Object)

Returns:

  • (Object)


169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/takagi/message/outbound.rb', line 169

def normalize_options(options)
  return {} unless options.is_a?(Hash)

  @logger.debug "Packet options are: #{options.inspect}"

  options.each_with_object({}) do |(key, value), acc|
    numeric_key = Integer(key)
    values = Array(value)
    acc[numeric_key] = values
  end
rescue ArgumentError
  {}
end

#serialize_payload(payload) ⇒ String?

Serialize payload based on content-format option

Parameters:

  • payload (Object)

    Payload data to serialize

Returns:

  • (String, nil)

    Serialized binary payload



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/takagi/message/outbound.rb', line 23

def serialize_payload(payload)
  return nil if payload.nil?

  # Already a string? Return as binary
  return payload.b if payload.is_a?(String)

  # Get content-format from options (default to JSON if not specified)
  content_format = @options[CoAP::Registries::Option::CONTENT_FORMAT]
  # Options are stored as arrays, extract first element
  content_format = content_format.first if content_format.is_a?(Array)
  content_format ||= CoAP::Registries::ContentFormat::JSON

  # Use serialization system
  Serialization::Registry.encode(payload, content_format)
rescue Serialization::UnknownFormatError
  # Fallback to JSON for unknown formats
  @logger.warn "Unknown content-format #{content_format}, falling back to JSON"
  payload.to_json.b
rescue Serialization::EncodeError => e
  @logger.error "Serialization failed: #{e.message}, falling back to JSON"
  payload.to_json.b
end

#to_bytes(transport: nil) ⇒ Object

Returns:

  • (Object)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/takagi/message/outbound.rb', line 46

def to_bytes(transport: nil)
  return ''.b unless @code

  # Use provided transport or fall back to instance variable
  actual_transport = transport || @transport

  with_error_handling do
    log_generation

    # NEW: Use transport registry for encoding
    packet = encode_with_transport(actual_transport)

    log_final_packet(packet)
    packet
  end
end

#token_bytesObject

Returns:

  • (Object)


130
131
132
# File 'lib/takagi/message/outbound.rb', line 130

def token_bytes
  @token.to_s.b
end

#with_error_handling { ... } ⇒ Object

Yields:

Yield Returns:

  • (Object)

Returns:

  • (Object)


89
90
91
92
93
94
# File 'lib/takagi/message/outbound.rb', line 89

def with_error_handling
  yield
rescue StandardError => e
  @logger.error "To_bytes failed: #{e.message} at #{e.backtrace.first}"
  ''.b
end