Module: Meshtastic::Serial

Defined in:
lib/meshtastic/serial.rb

Overview

rubocop:disable Metrics/ModuleLength

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. [email protected]



701
702
703
704
705
# File 'lib/meshtastic/serial.rb', line 701

public_class_method def self.authors
  "AUTHOR(S):
    0day Inc. <[email protected]>
  "
end

.connect(opts = {}) ⇒ Object

Supported Method Parameters

serial_obj = Meshtastic::Serial.connect( block_dev: 'optional - serial block device path (defaults to /dev/ttyUSB0)', baud: 'optional - (defaults to 115200)', data_bits: 'optional - (defaults to 8)', stop_bits: 'optional - (defaults to 1)', parity: 'optional - :even|:odd|:none (defaults to :none)', debug_out: 'optional - IO to receive non-protobuf debug console bytes', want_config: 'optional - request full node DB after connect (default: true)' )



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/meshtastic/serial.rb', line 242

public_class_method def self.connect(opts = {})
  block_dev = opts[:block_dev] ||= '/dev/ttyUSB0'
  raise "Invalid block device: #{block_dev}" unless File.exist?(block_dev)

  baud = opts[:baud] ||= 115_200
  data_bits = opts[:data_bits] ||= 8
  stop_bits = opts[:stop_bits] ||= 1
  parity = opts[:parity] ||= :none
  debug_out = opts[:debug_out]
  want_config = opts.fetch(:want_config, true)

  parity_char =
    case parity.to_s.to_sym
    when :even then 'E'
    when :odd  then 'O'
    when :none then 'N'
    else
      raise "Invalid parity: #{opts[:parity]}"
    end

  mode = "#{data_bits}#{parity_char}#{stop_bits}"

  clear_hupcl(block_dev: block_dev)

  serial_conn = UART.open(block_dev, baud, mode)

  serial_obj = {
    serial_conn: serial_conn,
    block_dev: block_dev,
    baud: baud,
    tx_mutex: Mutex.new,
    my_info: nil,
    my_node_num: nil,
    metadata: nil
  }

  serial_obj[:rx_thread] = init_rx_thread(
    serial_conn: serial_conn,
    serial_obj: serial_obj,
    debug_out: debug_out
  )
  @last_serial_obj = serial_obj

  # Wake / resync the device's framing state-machine.
  wake_up_device(serial_obj: serial_obj)

  if want_config
    mui = Meshtastic::MeshInterface.new
    to_radio_bytes = mui.start_config
    serial_obj[:config_id] = mui.config_id
    send_to_radio(serial_obj: serial_obj, to_radio: to_radio_bytes)
  end

  serial_obj
rescue StandardError => e
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
  raise e
end

.disconnect(opts = {}) ⇒ Object

Supported Method Parameters

serial_obj = Meshtastic::Serial.disconnect( serial_obj: 'required - serial_obj returned from #connect method' )



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
# File 'lib/meshtastic/serial.rb', line 660

public_class_method def self.disconnect(opts = {})
  serial_obj = opts[:serial_obj]
  return nil if serial_obj.nil? || serial_obj[:closing]

  serial_obj[:closing] = true
  serial_obj[:from_radio_queue]&.close
  serial_obj[:config_queue]&.close

  # Ask device to release the link (best-effort).
  begin
    if serial_obj[:serial_conn] && !serial_obj[:serial_conn].closed?
      to_radio = Meshtastic::ToRadio.new
      to_radio.disconnect = true
      send_to_radio(serial_obj: serial_obj, to_radio: to_radio)
      sleep 0.05
    end
  rescue StandardError
    # ignore during teardown
  end

  rx_thread = serial_obj[:rx_thread]
  serial_conn = serial_obj[:serial_conn]

  begin
    serial_conn&.close
  rescue StandardError
    nil
  end

  if rx_thread&.alive? && rx_thread != Thread.current
    rx_thread.join(1)
    rx_thread.kill if rx_thread.alive?
  end

  nil
rescue StandardError => e
  raise e
end

.drain_from_radio(opts = {}) ⇒ Object

Drain the FromRadio queue without blocking (returns Array of FromRadio msgs).



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/meshtastic/serial.rb', line 371

public_class_method def self.drain_from_radio(opts = {})
  max = opts[:max] ||= 256
  msgs = []
  serial_obj = opts[:serial_obj] || @last_serial_obj
  queue = serial_obj && serial_obj[:from_radio_queue]
  return msgs unless queue

  max.times do
    message = queue.pop(true)
    break unless message

    msgs << message
  rescue ThreadError
    break
  end
  msgs
end

.dump_stdout_data(opts = {}) ⇒ Object

Supported Method Parameters

stdout_data = Meshtastic::Serial.dump_stdout_data( type: 'required - :proto or :console' )



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/meshtastic/serial.rb', line 334

public_class_method def self.dump_stdout_data(opts = {})
  type = opts[:type]
  valid_types = i[proto console]
  raise "ERROR: Invalid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)

  serial_obj = opts[:serial_obj] || @last_serial_obj
  raise 'ERROR: call connect first' unless serial_obj

  data = serial_obj[:rx_mutex].synchronize do
    type == :proto ? serial_obj[:proto_data].dup : serial_obj[:console_data].join
  end
  return data unless block_given?

  (type == :proto ? data : data.split("\n")).each { |row| yield row } # rubocop:disable Style/ExplicitBlockArgument
  nil
end

.flush_data(opts = {}) ⇒ Object

rubocop:disable Naming/PredicateMethod



355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/meshtastic/serial.rb', line 355

public_class_method def self.flush_data(opts = {}) # rubocop:disable Naming/PredicateMethod
  type = opts[:type]
  valid_types = i[proto console]
  raise "ERROR: Invalid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)

  serial_obj = opts[:serial_obj] || @last_serial_obj
  raise 'ERROR: call connect first' unless serial_obj

  serial_obj[:rx_mutex].synchronize do
    serial_obj[:console_data].clear if type == :console
    serial_obj[:proto_data].clear if type == :proto
  end
  true
end

.helpObject

Display Usage for this Module



709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
# File 'lib/meshtastic/serial.rb', line 709

public_class_method def self.help
  puts "        USAGE:
    # Run the request class method for this module.
    #{self}.request(
      serial_obj: 'optional - value for serial_obj passed into request',
      payload: 'optional - value for payload passed into request'
    )

    # Run the send_to_radio class method for this module.
    #{self}.send_to_radio(
      serial_obj: 'optional - value for serial_obj passed into send_to_radio',
      to_radio: 'optional - value for to_radio passed into send_to_radio'
    )

    # Run the connect class method for this module.
    #{self}.connect(
      block_dev: 'optional - value for block_dev passed into connect',
      baud: 'optional - value for baud passed into connect',
      data_bits: 'optional - value for data_bits passed into connect',
      stop_bits: 'optional - value for stop_bits passed into connect',
      parity: 'optional - value for parity passed into connect',
      debug_out: 'optional - value for debug_out passed into connect'
    )

    # Run the wait_for_config class method for this module.
    #{self}.wait_for_config(
      serial_obj: 'optional - value for serial_obj passed into wait_for_config'
    )

    # Run the wake_up_device class method for this module.
    #{self}.wake_up_device(
      serial_obj: 'optional - value for serial_obj passed into wake_up_device'
    )

    # Run the dump_stdout_data class method for this module.
    #{self}.dump_stdout_data(
      type: 'optional - value for type passed into dump_stdout_data',
      serial_obj: 'optional - value for serial_obj passed into dump_stdout_data'
    )

    # Run the flush_data class method for this module.
    #{self}.flush_data(
      type: 'optional - value for type passed into flush_data',
      serial_obj: 'optional - value for serial_obj passed into flush_data'
    )

    # Run the drain_from_radio class method for this module.
    #{self}.drain_from_radio(
      max: 'optional - value for max passed into drain_from_radio',
      serial_obj: 'optional - value for serial_obj passed into drain_from_radio'
    )

    # Run the recv_from_radio class method for this module.
    #{self}.recv_from_radio(
      serial_obj: 'optional - value for serial_obj passed into recv_from_radio'
    )

    # Run the monitor_stdout class method for this module.
    #{self}.monitor_stdout(
      serial_obj: 'optional - value for serial_obj passed into monitor_stdout',
      type: 'optional - value for type passed into monitor_stdout',
      refresh: 'optional - value for refresh passed into monitor_stdout',
      include: 'optional - value for include passed into monitor_stdout',
      exclude: 'optional - value for exclude passed into monitor_stdout'
    )

    # Run the subscribe class method for this module.
    #{self}.subscribe(
      serial_obj: 'optional - value for serial_obj passed into subscribe',
      psks: 'optional - value for psks passed into subscribe',
      exclude: 'optional - value for exclude passed into subscribe',
      include: 'optional - value for include passed into subscribe',
      gps_metadata: 'optional - value for gps_metadata passed into subscribe',
      include_raw: 'optional - value for include_raw passed into subscribe',
      timeout: 'optional - value for timeout passed into subscribe'
    )

    # Run the send_text class method for this module.
    #{self}.send_text(
      serial_obj: 'optional - value for serial_obj passed into send_text',
      via: 'optional - value for via passed into send_text',
      channel: 'optional - value for channel passed into send_text',
      from: 'optional - value for from passed into send_text',
      psks: 'optional - value for psks passed into send_text',
      text: 'optional - value for text passed into send_text'
    )

    # Run the send_data class method for this module.
    #{self}.send_data(
      serial_obj: 'optional - value for serial_obj passed into send_data',
      via: 'optional - value for via passed into send_data',
      channel: 'optional - value for channel passed into send_data',
      psks: 'optional - value for psks passed into send_data',
      from: 'optional - value for from passed into send_data'
    )

    # Run the disconnect class method for this module.
    #{self}.disconnect(
      serial_obj: 'optional - value for serial_obj passed into disconnect'
    )

    # Run the authors class method for this module.
    #{self}.authors

  "
end

.monitor_stdout(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::Serial.monitor_stdout( serial_obj: 'required - serial_obj returned from #connect method', type: 'required - :proto or :console', refresh: 'optional - refresh interval (default: 3)', include: 'optional - comma-delimited string(s) to include in message (default: nil)', exclude: 'optional - comma-delimited string(s) to exclude in message (default: nil)' )



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/meshtastic/serial.rb', line 414

public_class_method def self.monitor_stdout(opts = {})
  serial_obj = opts[:serial_obj]
  type = opts[:type]
  valid_types = i[proto console]
  raise "ERROR: Invalid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)

  refresh = opts[:refresh] ||= 3
  include = opts[:include]
  exclude = opts[:exclude]

  loop do
    exclude_arr = exclude.to_s.split(',').map(&:strip)
    include_arr = include.to_s.split(',').map(&:strip)

    dump_stdout_data(serial_obj: serial_obj, type: type) do |data|
      data_s = data.is_a?(Hash) ? data.inspect : data.to_s
      disp = exclude_arr.none? { |exc| data_s.include?(exc) } && (
        include_arr.empty? ||
        include_arr.all? { |inc| data_s.include?(inc) }
      )
      puts data_s if disp
    end
    flush_data(serial_obj: serial_obj, type: type)
    sleep refresh
  end
rescue Interrupt
  puts "\nCTRL+C detected. Breaking out of console mode..."
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
rescue StandardError => e
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
  raise e
end

.recv_from_radio(opts = {}) ⇒ Object

Block until a FromRadio arrives or timeout (seconds). Returns FromRadio or nil.



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/meshtastic/serial.rb', line 390

public_class_method def self.recv_from_radio(opts = {})
  timeout = opts.fetch(:timeout, 5)
  serial_obj = opts[:serial_obj] || @last_serial_obj
  queue = serial_obj && serial_obj[:from_radio_queue]
  raise 'ERROR: RX queue not initialised — call connect first' unless queue

  message = if timeout.nil? || timeout.negative?
              queue.pop
            else
              queue.pop(timeout: timeout)
            end
  raise serial_obj[:rx_error] if message.nil? && serial_obj[:rx_error]

  message
end

.request(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::Serial.request( serial_obj: 'required serial_obj returned from #connect method', payload: 'required - array of bytes OR string to write to serial device' )



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/meshtastic/serial.rb', line 167

public_class_method def self.request(opts = {})
  serial_obj = opts[:serial_obj]
  serial_conn = serial_obj[:serial_conn]
  payload = opts[:payload]

  bytes =
    case payload
    when String then payload.b
    when Array  then payload.pack('C*')
    else
      raise "ERROR: Invalid payload type: #{payload.class}"
    end

  serial_obj[:tx_mutex] ||= Mutex.new
  serial_obj[:tx_mutex].synchronize do
    offset = 0
    while offset < bytes.bytesize
      count = serial_conn.write(bytes.byteslice(offset, bytes.bytesize - offset))
      raise IOError, 'serial write made no progress' unless count && count.positive?

      offset += count
    end
    serial_conn.flush
  end
  sleep 0.05
  bytes.bytesize
rescue StandardError => e
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
  raise e
end

.send_data(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::Serial.send_data( serial_obj: 'required - serial_obj returned from #connect method', ...same kwargs as MeshInterface#send_data (via forced to :radio) )



641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/meshtastic/serial.rb', line 641

public_class_method def self.send_data(opts = {})
  serial_obj = opts[:serial_obj]
  raise 'ERROR: serial_obj is required' unless serial_obj

  opts = opts.dup
  opts[:via] = :radio
  opts[:channel] ||= 0
  opts[:psks] = nil
  opts[:from] = serial_obj[:my_node_num] || 0 if opts[:from].nil?

  mui = Meshtastic::MeshInterface.new
  protobuf = mui.send_data(opts)
  send_to_radio(serial_obj: serial_obj, to_radio: protobuf)
end

.send_text(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::Serial.send_text( serial_obj: 'required - serial_obj returned from #connect method', from: 'optional - From ID (Default: local my_node_num or 0 for firmware-assigned)', to: 'optional - Destination ID (Default: "!ffffffff")', channel: 'optional - channel index (Default: 0)', text: 'optional - Text Message (Default: SYN)', want_ack: 'optional - Want Acknowledgement (Default: false)', want_response: 'optional - Want Response (Default: false)', hop_limit: 'optional - Hop Limit (Default: 3)', psks: 'optional - ignored for serial (device owns channel crypto)' )

Raises:

  • (ArgumentError)


614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# File 'lib/meshtastic/serial.rb', line 614

public_class_method def self.send_text(opts = {})
  serial_obj = opts[:serial_obj]
  raise 'ERROR: serial_obj is required' unless serial_obj

  opts = opts.dup
  opts[:via] = :radio
  opts[:channel] ||= 0

  opts[:from] = serial_obj[:my_node_num] || 0 if opts[:from].nil?

  # Device performs channel encryption for serial ToRadio packets.
  # Pass nil psks so MeshInterface leaves the payload in :decoded form.
  opts[:psks] = nil
  opts[:text] = opts.fetch(:text, 'SYN').to_s
  max_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
  raise ArgumentError, "ERROR: Text Length > #{max_len} Bytes" if opts[:text].bytesize > max_len

  mui = Meshtastic::MeshInterface.new
  protobuf = mui.send_text(opts)
  send_to_radio(serial_obj: serial_obj, to_radio: protobuf)
end

.send_to_radio(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::Serial.send_to_radio( serial_obj: 'required - serial_obj returned from #connect method', to_radio: 'required - Meshtastic::ToRadio OR already-serialized String' )



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/meshtastic/serial.rb', line 203

public_class_method def self.send_to_radio(opts = {})
  serial_obj = opts[:serial_obj]
  raise 'ERROR: serial_obj is required' unless serial_obj

  to_radio = opts[:to_radio]
  raise 'ERROR: to_radio is required' if to_radio.nil?

  body =
    case to_radio
    when String
      to_radio.b
    when Meshtastic::ToRadio
      to_radio.to_proto
    else
      raise "ERROR: to_radio must be Meshtastic::ToRadio or String, got #{to_radio.class}"
    end

  raise "ERROR: ToRadio payload too large (#{body.bytesize} > #{Meshtastic::MAX_TO_FROM_RADIO_SIZE})" if body.bytesize > Meshtastic::MAX_TO_FROM_RADIO_SIZE

  header = [
    Meshtastic::START1,
    Meshtastic::START2,
    (body.bytesize >> 8) & 0xFF,
    body.bytesize & 0xFF
  ].pack('C*')

  request(serial_obj: serial_obj, payload: header + body)
end

.subscribe(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::Serial.subscribe( serial_obj: 'required - serial_obj returned from #connect method', psks: 'optional - hash of :channel_id => psk (default: { LongFast: "AQ==" })', exclude: 'optional - comma-delimited substrings to hide', include: 'optional - comma-delimited substrings required to display', gps_metadata: 'optional - reverse-geocode POSITION payloads (default: false)', include_raw: 'optional - include raw protobuf bytes (default: false)', timeout: 'optional - seconds to block on empty queue per iteration (default: nil = forever)' ) Yields each decoded FromRadio hash. Without a block, pretty-prints packets.



511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/meshtastic/serial.rb', line 511

public_class_method def self.subscribe(opts = {})
  serial_obj = opts[:serial_obj]
  raise 'ERROR: serial_obj is required' unless serial_obj

  public_psk = '1PG7OiApB1nwvP+rz05pAQ=='
  psks = opts[:psks] ||= { LongFast: public_psk }
  raise 'ERROR: psks parameter must be a hash of :channel_id => psk key value pairs' unless psks.is_a?(Hash)

  psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
  mui = Meshtastic::MeshInterface.new
  psks = mui.get_cipher_keys(psks: psks)

  exclude = opts[:exclude]
  include = opts[:include]
   = opts[:gps_metadata] ||= false
  include_raw = opts[:include_raw] ||= false
  timeout = opts[:timeout]

  include_arr = include.to_s.split(',').map(&:strip)
  exclude_arr = exclude.to_s.split(',').map(&:strip)

  puts 'Subscribing to serial FromRadio stream...'

  loop do
    from_radio = recv_from_radio(serial_obj: serial_obj, timeout: timeout)
    break if from_radio.nil? && serial_obj[:from_radio_queue].closed?

    next if from_radio.nil?

    begin
      decoded_payload_hash = from_radio.to_h
      raw_packet = from_radio.to_proto if include_raw

      message = {}
      stdout_message = ''

      if decoded_payload_hash[:packet].is_a?(Hash)
        message = enrich_packet(
          message: decoded_payload_hash[:packet],
          psks: psks,
          gps_metadata: ,
          include_raw: include_raw,
          raw_packet: raw_packet
        )
        decoded_payload_hash[:packet] = message
      end

      unless block_given?
        message[:stdout] = 'pretty' if message.is_a?(Hash)
        stdout_message = JSON.pretty_generate(decoded_payload_hash)
      end
    rescue Encoding::CompatibilityError,
           Google::Protobuf::ParseError,
           JSON::GeneratorError,
           ArgumentError => e
      message[:decrypted] = e.message if message.is_a?(Hash)
      decoded_payload_hash[:packet] = message if message.is_a?(Hash)
      unless block_given?
        message[:stdout] = 'inspect' if message.is_a?(Hash)
        stdout_message = decoded_payload_hash.inspect
      end
    ensure
      flat_source = decoded_payload_hash.is_a?(Hash) ? decoded_payload_hash : {}
      flat_message = flat_source.values.join(' ')
      flat_message = "#{flat_message} #{message.values.join(' ')}" if message.is_a?(Hash)

      disp = exclude_arr.none? { |exc| flat_message.include?(exc) } &&
             include_arr.all? { |inc| flat_message.include?(inc) }

      if disp
        if block_given?
          yield decoded_payload_hash
        else
          puts "\n"
          puts '-' * 80
          puts 'MSG:'
          puts stdout_message
          puts '-' * 80
          puts "\n\n\n"
        end
      end
    end
  end
rescue Interrupt
  puts "\nCTRL+C detected. Exiting..."
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
rescue StandardError => e
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
  raise e
end

.wait_for_config(opts = {}) ⇒ Object

Wait for the requested configuration without consuming application messages. Raises Timeout::Error if the firmware does not complete the handshake.

Raises:

  • (ArgumentError)


303
304
305
306
307
308
309
310
311
312
313
# File 'lib/meshtastic/serial.rb', line 303

public_class_method def self.wait_for_config(opts = {})
  serial_obj = opts[:serial_obj]
  raise ArgumentError, 'serial_obj with want_config enabled is required' unless serial_obj && serial_obj[:config_id]

  serial_obj[:config_queue].pop(timeout: opts.fetch(:timeout, 10))
  raise serial_obj[:rx_error] if serial_obj[:rx_error]
  raise IOError, 'serial connection closed' if serial_obj[:closing]
  raise Timeout::Error, "No configuration response from #{serial_obj[:block_dev]}" unless serial_obj[:config_complete]

  serial_obj
end

.wake_up_device(opts = {}) ⇒ Object

Supported Method Parameters

wake_up_device( serial_obj: 'required - serial_obj returned from #connect method' )



319
320
321
322
323
324
325
326
327
328
# File 'lib/meshtastic/serial.rb', line 319

public_class_method def self.wake_up_device(opts = {})
  serial_obj = opts[:serial_obj]
  # START2 * 32 — does not look like a valid header, forces RX state machine resync
  start2_bytes = ([Meshtastic::START2] * 32).pack('C*')
  request(serial_obj: serial_obj, payload: start2_bytes)
  sleep 0.1
rescue StandardError => e
  disconnect(serial_obj: serial_obj) unless serial_obj.nil?
  raise e
end