Class: Rpremote::PicoModem

Inherits:
Object
  • Object
show all
Defined in:
lib/rpremote/picomodem.rb,
sig/rpremote.rbs

Defined Under Namespace

Classes: ChecksumError, DeviceError, Error, ProtocolError, TimeoutError

Constant Summary collapse

STX =
0x02
ACK =
0x06
FILE_READ =
0x01
FILE_WRITE =
0x02
DFU_START =

Returns:

  • (Integer)
0x03
CHUNK =
0x04
ABORT =
0xFF
FILE_DATA =
0x81
FILE_ACK =
0x82
DFU_ACK =

Returns:

  • (Integer)
0x83
CHUNK_ACK =
0x84
DONE_ACK =
0x8F
ERROR =
0xFE
OK =
0x00
READY =
0x01
CHUNK_SIZE =

Returns:

  • (Integer)
480
DEFAULT_TIMEOUT =

Returns:

  • (Float)
20.0
MAX_FRAME_BODY =
65_535
DFU_MAGIC =

Returns:

  • (String)
"DFU\0".b
DFU_VERSION =

Returns:

  • (Integer)
1
DFU_TYPES =

Returns:

  • (Array[String])
%w[RUBY RITE].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io, timeout: DEFAULT_TIMEOUT) ⇒ PicoModem

Returns a new instance of PicoModem.

Parameters:

  • io (Object)
  • timeout: (Float) (defaults to: DEFAULT_TIMEOUT)

Raises:

  • (ArgumentError)


41
42
43
44
45
46
# File 'lib/rpremote/picomodem.rb', line 41

def initialize(io, timeout: DEFAULT_TIMEOUT)
  raise ArgumentError, "timeout must be positive" unless timeout.positive?

  @io = io
  @timeout = timeout
end

Instance Attribute Details

#ioObject (readonly)

Returns the value of attribute io.

Returns:

  • (Object)


39
40
41
# File 'lib/rpremote/picomodem.rb', line 39

def io
  @io
end

#timeoutFloat (readonly)

Returns the value of attribute timeout.

Returns:

  • (Float)


39
40
41
# File 'lib/rpremote/picomodem.rb', line 39

def timeout
  @timeout
end

Instance Method Details

#dfu(data, type:) ⇒ Integer

Parameters:

  • data (String)
  • type: (String)

Returns:

  • (Integer)


106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/rpremote/picomodem.rb', line 106

def dfu(data, type:)
  data = data.b
  type = validate_dfu_type(type)
  header = [DFU_MAGIC, DFU_VERSION, type, data.bytesize, Checksum.crc32(data), 0].pack("a4Ca4NNn")
  in_session do
    send_frame(DFU_START, header)
    expect_status(DFU_ACK, READY, "DFU readiness")
    send_chunks(data)
    expect_dfu_completion
  end
  data.bytesize
end

#download(path) ⇒ String

Parameters:

  • path (String)

Returns:

  • (String)


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/rpremote/picomodem.rb', line 65

def download(path)
  path = validate_path(path)
  data = +"".b
  total = nil

  in_session do
    send_frame(FILE_READ, path)
    loop do
      command, payload = receive_frame
      case command
      when FILE_DATA
        if total.nil?
          raise ProtocolError, "first FILE_DATA frame has no size" if payload.bytesize < 4

          total = payload.unpack1("N")
          data << payload.byteslice(4..)
        else
          data << payload
        end
        raise ProtocolError, "received more bytes than declared" if total && data.bytesize > total

        send_frame(CHUNK_ACK, [OK].pack("C"))
      when DONE_ACK
        status, remote_crc = parse_completion(payload)
        raise DeviceError, format("download failed with status 0x%02x", status) unless status == OK
        raise ProtocolError, "file size mismatch: expected #{total}, received #{data.bytesize}" if total && data.bytesize != total

        local_crc = Checksum.crc32(data)
        raise ChecksumError, checksum_mismatch("CRC32", local_crc, remote_crc, 8) unless remote_crc == local_crc

        break
      when ERROR
        raise DeviceError, payload.force_encoding(Encoding::UTF_8)
      else
        raise ProtocolError, format("unexpected response 0x%02x during download", command)
      end
    end
  end
  data
end

#receive_frame[Integer, String]

Returns:

  • ([Integer, String])

Raises:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/rpremote/picomodem.rb', line 128

def receive_frame
  deadline = monotonic_time + timeout
  scan_for(STX, deadline, "PicoModem frame")
  length = read_exact(2, deadline).unpack1("n")
  raise ProtocolError, "invalid empty frame" if length.zero?
  raise ProtocolError, "frame body is too large: #{length}" if length > MAX_FRAME_BODY

  body = read_exact(length, deadline)
  expected_crc = read_exact(2, deadline).unpack1("n")
  actual_crc = Checksum.crc16(body)
  raise ChecksumError, checksum_mismatch("CRC16", actual_crc, expected_crc, 4) unless actual_crc == expected_crc

  [body.getbyte(0), body.byteslice(1..) || +"".b]
end

#send_frame(command, payload = +"".b)) ⇒ Integer

Parameters:

  • command (Integer)
  • payload (String) (defaults to: +"".b))

Returns:

  • (Integer)

Raises:



119
120
121
122
123
124
125
126
# File 'lib/rpremote/picomodem.rb', line 119

def send_frame(command, payload = +"".b)
  body = [command].pack("C") + payload.b
  raise ProtocolError, "frame body is too large" if body.bytesize > MAX_FRAME_BODY

  frame = [STX, body.bytesize].pack("Cn") + body + [Checksum.crc16(body)].pack("n")
  write_all(frame)
  frame.bytesize
end

#upload(path, data) ⇒ Integer

Parameters:

  • path (String)
  • data (String)

Returns:

  • (Integer)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/rpremote/picomodem.rb', line 48

def upload(path, data)
  path = validate_path(path)
  data = data.b
  in_session do
    send_frame(FILE_WRITE, [data.bytesize].pack("N") + path)
    expect_status(FILE_ACK, READY, "device readiness")
    send_chunks(data)

    status, remote_crc = completion
    raise DeviceError, format("upload failed with status 0x%02x", status) unless status == OK

    local_crc = Checksum.crc32(data)
    raise ChecksumError, checksum_mismatch("CRC32", local_crc, remote_crc, 8) unless remote_crc == local_crc
  end
  data.bytesize
end