maxcube-client 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rubocop.yml +0 -3
  4. data/.yardopts +1 -0
  5. data/README.md +17 -2
  6. data/bin/maxcube-client +2 -27
  7. data/doc/MaxCube.html +502 -0
  8. data/doc/MaxCube/Messages.html +1927 -0
  9. data/doc/MaxCube/Messages/Handler.html +2912 -0
  10. data/doc/MaxCube/Messages/InvalidMessage.html +140 -0
  11. data/doc/MaxCube/Messages/InvalidMessageBody.html +264 -0
  12. data/doc/MaxCube/Messages/InvalidMessageFormat.html +247 -0
  13. data/doc/MaxCube/Messages/InvalidMessageLength.html +247 -0
  14. data/doc/MaxCube/Messages/InvalidMessageType.html +263 -0
  15. data/doc/MaxCube/Messages/Parser.html +520 -0
  16. data/doc/MaxCube/Messages/Serializer.html +701 -0
  17. data/doc/MaxCube/Messages/TCP.html +172 -0
  18. data/doc/MaxCube/Messages/TCP/Handler.html +1396 -0
  19. data/doc/MaxCube/Messages/TCP/Parser.html +462 -0
  20. data/doc/MaxCube/Messages/TCP/Parser/MessageA.html +186 -0
  21. data/doc/MaxCube/Messages/TCP/Parser/MessageC.html +1077 -0
  22. data/doc/MaxCube/Messages/TCP/Parser/MessageF.html +206 -0
  23. data/doc/MaxCube/Messages/TCP/Parser/MessageH.html +338 -0
  24. data/doc/MaxCube/Messages/TCP/Parser/MessageL.html +535 -0
  25. data/doc/MaxCube/Messages/TCP/Parser/MessageM.html +510 -0
  26. data/doc/MaxCube/Messages/TCP/Parser/MessageN.html +226 -0
  27. data/doc/MaxCube/Messages/TCP/Parser/MessageS.html +225 -0
  28. data/doc/MaxCube/Messages/TCP/Serializer.html +460 -0
  29. data/doc/MaxCube/Messages/TCP/Serializer/MessageA.html +186 -0
  30. data/doc/MaxCube/Messages/TCP/Serializer/MessageC.html +185 -0
  31. data/doc/MaxCube/Messages/TCP/Serializer/MessageF.html +206 -0
  32. data/doc/MaxCube/Messages/TCP/Serializer/MessageL.html +185 -0
  33. data/doc/MaxCube/Messages/TCP/Serializer/MessageM.html +428 -0
  34. data/doc/MaxCube/Messages/TCP/Serializer/MessageN.html +209 -0
  35. data/doc/MaxCube/Messages/TCP/Serializer/MessageQ.html +185 -0
  36. data/doc/MaxCube/Messages/TCP/Serializer/MessageS.html +1168 -0
  37. data/doc/MaxCube/Messages/TCP/Serializer/MessageT.html +240 -0
  38. data/doc/MaxCube/Messages/TCP/Serializer/MessageU.html +206 -0
  39. data/doc/MaxCube/Messages/TCP/Serializer/MessageZ.html +252 -0
  40. data/doc/MaxCube/Messages/UDP.html +164 -0
  41. data/doc/MaxCube/Messages/UDP/Handler.html +832 -0
  42. data/doc/MaxCube/Messages/UDP/Parser.html +609 -0
  43. data/doc/MaxCube/Messages/UDP/Parser/MessageH.html +218 -0
  44. data/doc/MaxCube/Messages/UDP/Parser/MessageI.html +215 -0
  45. data/doc/MaxCube/Messages/UDP/Parser/MessageN.html +226 -0
  46. data/doc/MaxCube/Messages/UDP/Serializer.html +484 -0
  47. data/doc/MaxCube/Network.html +167 -0
  48. data/doc/MaxCube/Network/TCP.html +150 -0
  49. data/doc/MaxCube/Network/TCP/Client.html +1930 -0
  50. data/doc/MaxCube/Network/TCP/Client/Commands.html +2457 -0
  51. data/doc/MaxCube/Network/TCP/SampleServer.html +910 -0
  52. data/doc/MaxCube/Network/UDP.html +150 -0
  53. data/doc/MaxCube/Network/UDP/Client.html +518 -0
  54. data/doc/MaxCube/Network/UDP/SampleSocket.html +628 -0
  55. data/doc/MaxCube/Runner.html +355 -0
  56. data/doc/_index.html +518 -0
  57. data/doc/class_list.html +51 -0
  58. data/doc/css/common.css +1 -0
  59. data/doc/css/full_list.css +58 -0
  60. data/doc/css/style.css +499 -0
  61. data/doc/file.README.html +140 -0
  62. data/doc/file_list.html +56 -0
  63. data/doc/frames.html +17 -0
  64. data/doc/index.html +140 -0
  65. data/doc/js/app.js +248 -0
  66. data/doc/js/full_list.js +216 -0
  67. data/doc/js/jquery.js +4 -0
  68. data/doc/method_list.html +1699 -0
  69. data/doc/top-level-namespace.html +110 -0
  70. data/lib/maxcube.rb +11 -0
  71. data/lib/maxcube/messages.rb +85 -8
  72. data/lib/maxcube/messages/handler.rb +138 -4
  73. data/lib/maxcube/messages/parser.rb +33 -2
  74. data/lib/maxcube/messages/serializer.rb +64 -16
  75. data/lib/maxcube/messages/tcp.rb +11 -7
  76. data/lib/maxcube/messages/tcp/handler.rb +50 -2
  77. data/lib/maxcube/messages/tcp/parser.rb +18 -17
  78. data/lib/maxcube/messages/tcp/serializer.rb +20 -21
  79. data/lib/maxcube/messages/tcp/type/a.rb +6 -6
  80. data/lib/maxcube/messages/tcp/type/c.rb +5 -3
  81. data/lib/maxcube/messages/tcp/type/f.rb +5 -3
  82. data/lib/maxcube/messages/tcp/type/h.rb +3 -2
  83. data/lib/maxcube/messages/tcp/type/l.rb +8 -7
  84. data/lib/maxcube/messages/tcp/type/m.rb +11 -7
  85. data/lib/maxcube/messages/tcp/type/n.rb +5 -3
  86. data/lib/maxcube/messages/tcp/type/q.rb +2 -2
  87. data/lib/maxcube/messages/tcp/type/s.rb +5 -2
  88. data/lib/maxcube/messages/tcp/type/t.rb +5 -4
  89. data/lib/maxcube/messages/tcp/type/u.rb +2 -1
  90. data/lib/maxcube/messages/tcp/type/z.rb +4 -2
  91. data/lib/maxcube/messages/udp.rb +7 -0
  92. data/lib/maxcube/messages/udp/handler.rb +28 -0
  93. data/lib/maxcube/messages/udp/parser.rb +23 -6
  94. data/lib/maxcube/messages/udp/serializer.rb +17 -1
  95. data/lib/maxcube/messages/udp/type/h.rb +2 -0
  96. data/lib/maxcube/messages/udp/type/i.rb +3 -0
  97. data/lib/maxcube/messages/udp/type/n.rb +3 -0
  98. data/lib/maxcube/network.rb +5 -1
  99. data/lib/maxcube/network/tcp.rb +3 -0
  100. data/lib/maxcube/network/tcp/client.rb +117 -3
  101. data/lib/maxcube/network/tcp/client/commands.rb +306 -239
  102. data/lib/maxcube/network/tcp/sample_server.rb +1 -0
  103. data/lib/maxcube/network/udp.rb +3 -0
  104. data/lib/maxcube/network/udp/client.rb +2 -0
  105. data/lib/maxcube/network/udp/sample_socket.rb +1 -0
  106. data/lib/maxcube/runner.rb +45 -0
  107. data/lib/maxcube/version.rb +2 -1
  108. data/maxcube-client.gemspec +2 -0
  109. metadata +84 -3
@@ -4,14 +4,27 @@ require 'maxcube/messages/serializer'
4
4
  module MaxCube
5
5
  module Messages
6
6
  module UDP
7
+ # Extends {Messages::Serializer} and {UDP::Handler} of routines
8
+ # connected to UDP Cube messages serializing.
7
9
  class Serializer
8
- include Handler
10
+ include UDP::Handler
9
11
  include Messages::Serializer
10
12
 
13
+ # Known message types in the direction client -> Cube.
11
14
  MSG_TYPES = %w[I N h c R].freeze
12
15
 
16
+ # {UDP::MSG_PREFIX} with a suffix.
13
17
  MSG_PREFIX = (UDP::MSG_PREFIX + "*\x00").freeze
14
18
 
19
+ # Serializes data from a single hash
20
+ # into UDP Cube message.
21
+ # Calls {#check_udp_hash} at the begin
22
+ # and {#check_udp_msg} at the end.
23
+ # @param hash [Hash] particular message contents separated into hash.
24
+ # @option hash [String] :serial_number if not specified,
25
+ # it is set to universal value.
26
+ # It is used for broadcast messages.
27
+ # @return [String] output message.
15
28
  def serialize_udp_hash(hash)
16
29
  check_udp_hash(hash)
17
30
  serial_number = hash[:serial_number] || '*' * 10
@@ -21,6 +34,9 @@ module MaxCube
21
34
 
22
35
  private
23
36
 
37
+ # Tells how to get message type from a message.
38
+ # @param msg [String] input message.
39
+ # @return [String] message type.
24
40
  def msg_msg_type(msg)
25
41
  msg[18]
26
42
  end
@@ -3,9 +3,11 @@ module MaxCube
3
3
  module Messages
4
4
  module UDP
5
5
  class Parser
6
+ # Get URL information message.
6
7
  module MessageH
7
8
  private
8
9
 
10
+ # Mandatory keys.
9
11
  KEYS = (Parser::KEYS + %i[port url path]).freeze
10
12
 
11
13
  def parse_udp_h(_body)
@@ -3,9 +3,12 @@ module MaxCube
3
3
  module Messages
4
4
  module UDP
5
5
  class Parser
6
+ # Identify message.
7
+ # It can be used in broadcast.
6
8
  module MessageI
7
9
  private
8
10
 
11
+ # Mandatory keys.
9
12
  KEYS = (Parser::KEYS + %i[unknown
10
13
  rf_address firmware_version]).freeze
11
14
 
@@ -3,10 +3,13 @@ module MaxCube
3
3
  module Messages
4
4
  module UDP
5
5
  class Parser
6
+ # Get network address message.
6
7
  module MessageN
7
8
  private
8
9
 
10
+ # Local keys without the common ones.
9
11
  N_KEYS = %i[ip_address gateway subnet_mask dns1 dns2].freeze
12
+ # Mandatory keys.
10
13
  KEYS = (Parser::KEYS + N_KEYS).freeze
11
14
 
12
15
  def parse_udp_n(_body)
@@ -1,13 +1,17 @@
1
1
  require 'socket'
2
- require 'thread'
3
2
 
4
3
  require 'yaml'
5
4
 
6
5
  require 'maxcube'
7
6
 
8
7
  module MaxCube
8
+ # Encapsulates network structures providing clients and servers
9
+ # that comply Cube messages protocol.
10
+ # It utilizes parsing and serializing features from {Messages}.
9
11
  module Network
12
+ # Common localhost IP address.
10
13
  LOCALHOST = 'localhost'.freeze
14
+ # Common broadcast IP address.
11
15
  BROADCAST = '<broadcast>'.freeze
12
16
  end
13
17
  end
@@ -4,7 +4,10 @@ require 'maxcube/messages/tcp/serializer'
4
4
 
5
5
  module MaxCube
6
6
  module Network
7
+ # This module contains classes aimed onto TCP network tools
8
+ # related to Cube protocol.
7
9
  module TCP
10
+ # Common port used in Cube TCP communication.
8
11
  PORT = 62_910
9
12
  end
10
13
  end
@@ -4,8 +4,37 @@ require_relative 'client/commands'
4
4
  module MaxCube
5
5
  module Network
6
6
  module TCP
7
+ # Fundamental class that provides TCP communication
8
+ # with Cube gateway and connected devices.
9
+ # After connecting to Cube ({#connect}),
10
+ # interactive shell is launched.
11
+ #
12
+ # Communication with Cube is performed via messages,
13
+ # whereas client works with hashes,
14
+ # which have particular message contents divided
15
+ # and is human readable.
16
+ # An issue is how to pass contents of hashes
17
+ # as arguments of message serialization.
18
+ # For simple hashes client provides and option
19
+ # to pass arguments explicitly on command line.
20
+ # This would be difficult to accomplish
21
+ # for large hashes with subhashes,
22
+ # so YAML files are used in these cases,
23
+ # which are able to be generated both automatically and manually.
24
+ # This file has to be loaded into internal hash before each such message.
25
+ #
26
+ # Client interactive shell contains quite detailed usage message.
7
27
  class Client
8
- def initialize
28
+ # Default verbose mode on startup.
29
+ DEFAULT_VERBOSE = true
30
+ # Default persist mode on startup.
31
+ DEFAULT_PERSIST = true
32
+
33
+ # Creates all necessary internal variables.
34
+ # Internal hash is invalid on startup.
35
+ # @param verbose [Boolean] verbose mode on startup.
36
+ # @param persist [Boolean] persist mode on startup.
37
+ def initialize(verbose: DEFAULT_VERBOSE, persist: DEFAULT_PERSIST)
9
38
  @parser = Messages::TCP::Parser.new
10
39
  @serializer = Messages::TCP::Serializer.new
11
40
  @queue = Queue.new
@@ -22,16 +51,27 @@ module MaxCube
22
51
  @load_data_dir = @data_dir + 'load'
23
52
  @save_data_dir = @data_dir + 'save'
24
53
 
25
- @verbose = true
26
- @persist = true
54
+ @verbose = verbose
55
+ @persist = persist
27
56
  end
28
57
 
58
+ # Connects to concrete address and starts interactive shell ({#shell}).
59
+ # Calls {#receiver} in separate thread to receive all incoming messages.
60
+ # @param host remote host address.
61
+ # @param port remote host port.
29
62
  def connect(host = LOCALHOST, port = PORT)
30
63
  @socket = TCPSocket.new(host, port)
31
64
  @thread = Thread.new(self, &:receiver)
32
65
  shell
33
66
  end
34
67
 
68
+ # Routine started in separate thread
69
+ # that receives and parses all incoming messages in loop
70
+ # and stores them info thread-safe queue.
71
+ # Parsing is done via
72
+ # {Messages::TCP::Parser#parse_tcp_msg}.
73
+ # It should close gracefully on any +IOError+
74
+ # or on shell's initiative.
35
75
  def receiver
36
76
  puts '<Starting receiver thread ...>'
37
77
  while (data = @socket.gets)
@@ -50,6 +90,17 @@ module MaxCube
50
90
  puts e.to_s.capitalize
51
91
  end
52
92
 
93
+ # Interactive shell that maintains all operations with Cube.
94
+ # It is yet only simple +STDIN+ parser
95
+ # without any command history and other features
96
+ # that possess all decent shells.
97
+ # It calls {#command} on every input.
98
+ # It provides quite detailed usage message ({#cmd_usage}).
99
+ #
100
+ # It should close gracefully
101
+ # from user's will, when connection closes,
102
+ # or when soft interrupt appears.
103
+ # Calls {#close} when closing.
53
104
  def shell
54
105
  puts "Welcome to interactive shell!\n" \
55
106
  "Type 'help' for list of commands.\n\n"
@@ -64,6 +115,7 @@ module MaxCube
64
115
  close
65
116
  end
66
117
 
118
+ # Closes client gracefully.
67
119
  def close
68
120
  STDIN.close
69
121
  send_msg('q')
@@ -73,6 +125,9 @@ module MaxCube
73
125
 
74
126
  private
75
127
 
128
+ # Moves contents of receiver's queue to internal buffer.
129
+ # Queue is being filled from {#receiver}.
130
+ # Operation is thread-safe.
76
131
  def refresh_buffer
77
132
  until @queue.empty?
78
133
  data, hashes = @queue.pop
@@ -81,11 +136,22 @@ module MaxCube
81
136
  end
82
137
  end
83
138
 
139
+ # Returns only current or all (without or with history)
140
+ # collected part of buffer and history
141
+ # (contents of buffer is moved to history on clear command).
142
+ # @param dir_key [:recv, :sent] received or sent data.
143
+ # @param data_key [:hashes, :data] hashes or raw data (set of messages).
144
+ # @param history [Boolean] whether to include history.
145
+ # @return [Array<Hash>, String] demanded data.
84
146
  def buffer(dir_key, data_key, history = false)
85
147
  return @buffer[dir_key][data_key] unless history
86
148
  @history[dir_key][data_key] + @buffer[dir_key][data_key]
87
149
  end
88
150
 
151
+ # Executes command from shell command line.
152
+ # It calls a method dynamically according to {COMMANDS},
153
+ # or displays usage message {#cmd_usage}.
154
+ # @param line [String] command line from +STDIN+
89
155
  def command(line)
90
156
  cmd, *args = line.chomp.split
91
157
  return nil unless cmd
@@ -102,6 +168,19 @@ module MaxCube
102
168
  cmd_usage
103
169
  end
104
170
 
171
+ include Commands
172
+
173
+ # Zips +args+ with appropriate keys
174
+ # according to {Messages::Handler#msg_type_hash_keys}
175
+ # and {Messages::Handler#msg_type_hash_opt_keys}.
176
+ # @param type [String] message type.
177
+ # @param args [Array<String>] arguments from command line.
178
+ # @param opts [Hash] options that modifies interpreting of +args+.
179
+ # @option opts [Boolean] :last_array whether to insert
180
+ # all rest arguments into array that will be stored into the last key.
181
+ # @option opts [Boolean] :array_nonempty whether to require +last_array+
182
+ # _not_ to be empty.
183
+ # @return [Hash, nil] resulting hash, or +nil+ on failure.
105
184
  def send_msg_hash_from_keys_args(type, *args, **opts)
106
185
  keys = @serializer.msg_type_hash_keys(type) +
107
186
  @serializer.msg_type_hash_opt_keys(type)
@@ -118,18 +197,40 @@ module MaxCube
118
197
  keys.zip(args).to_h.reject { |_, v| v.nil? }
119
198
  end
120
199
 
200
+ # Returns hash via {#cmd_load}.
201
+ # It is used to combine sending a message with loading a hash from file.
202
+ # On success and in non-persistive mode,
203
+ # it simultaneously invalidates internal hash flag.
204
+ # @param args [Array<String>] arguments from command line.
205
+ # @return [Hash, nil] loaded hash, or +nil+ on failure.
121
206
  def send_msg_hash_from_internal(*args, **_opts)
122
207
  return nil unless cmd_load(*args.drop(1))
123
208
  @hash_set = false unless @persist
124
209
  @hash
125
210
  end
126
211
 
212
+ # Command line token that enables loading arguments (hash) from file.
127
213
  ARGS_FROM_HASH = '-'.freeze
128
214
 
215
+ # @param args [Array<String>] arguments from command line.
216
+ # @return [Boolean] whether to enable loading arguments (hash)
217
+ # from file.
129
218
  def args_from_hash?(args)
130
219
  args.first == ARGS_FROM_HASH
131
220
  end
132
221
 
222
+ # Returns hash with contents necessary for serialization
223
+ # of message of given message type.
224
+ # It is either built from command line +args+
225
+ # ({#send_msg_hash_from_keys_args}),
226
+ # or loaded from YAML file ({#send_msg_hash_from_internal}).
227
+ # @param type [String] message type.
228
+ # @param args [Array<String>] arguments from command line.
229
+ # @param opts [Hash] options that modifies interpreting of +args+.
230
+ # @option opts [Boolean] :load_only means that hash
231
+ # must be loaded from file (contents are too complex).
232
+ # Specifying {ARGS_FROM_HASH} is optional in this case.
233
+ # @return [Hash] resulting hash.
133
234
  def send_msg_hash(type, *args, **opts)
134
235
  if opts[:load_only] && !args_from_hash?(args)
135
236
  args.unshift(ARGS_FROM_HASH)
@@ -143,6 +244,17 @@ module MaxCube
143
244
  send_msg_hash_from_keys_args(type, *args, **opts)
144
245
  end
145
246
 
247
+ # Performs message serialization and sends it to Cube.
248
+ # It builds the hash to serialize from by {#send_msg_hash},
249
+ # and serializes it with
250
+ # {Messages::TCP::Serializer#serialize_tcp_hash}.
251
+ #
252
+ # Both sent message and built hash are buffered.
253
+ #
254
+ # It catches all {Messages::InvalidMessage} exceptions.
255
+ # @param type [String] message type.
256
+ # @param args [Array<String>] arguments from command line.
257
+ # @param opts [Hash] options that modifies interpreting of +args+.
146
258
  def send_msg(type, *args, **opts)
147
259
  hash = send_msg_hash(type, *args, **opts)
148
260
  return unless hash
@@ -165,6 +277,8 @@ module MaxCube
165
277
  puts e.to_s.capitalize
166
278
  end
167
279
 
280
+ # Prints hash in human readable way.
281
+ # @param hash [Hash] input hash.
168
282
  def print_hash(hash)
169
283
  puts hash.to_yaml
170
284
  end
@@ -3,282 +3,349 @@ module MaxCube
3
3
  module Network
4
4
  module TCP
5
5
  class Client
6
- private
7
-
8
- COMMANDS = {
9
- 'usage' => %w[? h help],
10
- 'data' => %w[B buffer d],
11
- 'history' => %w[H hist],
12
- 'clear' => %w[C],
13
- 'dump' => %w[D],
14
- 'list' => %w[l],
15
- 'config' => %w[c],
16
- 'send' => %w[cmd s set],
17
- 'pair' => %w[n],
18
- 'ntp' => %w[N f],
19
- 'url' => %w[U u],
20
- 'wake' => %w[w z],
21
- 'metadata' => %w[m meta],
22
- 'delete' => %w[del],
23
- 'reset' => %w[],
24
- 'verbose' => %w[V],
25
- 'save' => %w[S],
26
- 'load' => %w[L],
27
- 'persist' => %w[P],
28
- 'quit' => %w[q],
29
- }.freeze
30
-
31
- def usage_line(command, args, description,
32
- message = nil, response = nil)
33
- cmds_str = (COMMANDS[command].dup << command).join('|')
34
- cmds_str << ' ' << args unless args.empty?
35
-
36
- description, *rest = description.split("\n")
37
- rest << "[#{message} message]" if message
38
- rest << "[#{response} response]" if response
39
- rest = if rest.empty?
40
- ''
41
- else
42
- rest.map { |s| ' ' * 52 + s }.join("\n") << "\n"
43
- end
44
-
45
- ' ' << cmds_str << ' ' * (48 - cmds_str.size) <<
46
- description << "\n" << rest
47
- end
48
-
49
- def cmd_usage
50
- puts "\nUSAGE: <command> [<arguments...>]\nCOMMADS:\n" <<
51
- usage_line('usage', '',
52
- 'Prints this message') <<
53
- usage_line('data', '',
54
- 'Lists buffered received data (hashes)') <<
55
- usage_line('history', '',
56
- 'Lists all received data incl. the cleared') <<
57
- usage_line('clear', '',
58
- "Clears collected data\n" \
59
- '(resp. moves it to history)') <<
60
- usage_line('dump', '',
61
- "Shortcut for 'data' + 'clear'") <<
62
- usage_line('list', '',
63
- 'Requests for new list of devices', 'l', 'L') <<
64
- usage_line('config', '',
65
- 'Requests for configuration message', 'c', 'C') <<
66
- usage_line('send', '{}',
67
- 'Sends settings to connected devices',
68
- 's', 'S') <<
69
- usage_line('pair', '{<timeout>}',
70
- 'Sets device into pairing mode' \
71
- " with optional timeout\n" \
72
- '(request for a new device)', 'n', 'N') <<
73
- usage_line('ntp', '{<NTP servers...>}',
74
- 'Requests for NTP servers' \
75
- ' and optionally updates them',
76
- 'f', 'F') <<
77
- usage_line('url', '{<URL> <port>}',
78
- 'Configures Cube\'s portal URL', 'u') <<
79
- usage_line('wake', '{<time> <scope> [<ID>]}',
80
- 'Wake-ups the Cube',
81
- 'z', 'A') <<
82
- usage_line('metadata', '{}',
83
- 'Serializes metadata for the Cube',
84
- 'm', 'M') <<
85
- usage_line('delete', '{<count> <force> <RF addresses...>}',
86
- 'Deletes one or more devices from the Cube (!)',
87
- 't', 'A') <<
88
- usage_line('reset', '',
89
- 'Requests for factory reset (!)', 'a', 'A') <<
90
- usage_line('verbose', '',
91
- "Toggles verbose mode (whether is incoming data\n" \
92
- 'printed immediately or is not printed)') <<
93
- usage_line('save', '[a|A|all]',
94
- "Saves buffered [all] received and sent data\n" \
95
- "into files at '#{@save_data_dir}'") <<
96
- usage_line('load', '[<path>]',
97
- 'Loads first hash from YAML file' \
98
- " to internal variable\n" \
99
- "-> to pass data with outgoing message\n" \
100
- 'If path is relative,' \
101
- " it looks in '#{@load_data_dir}'\n" \
102
- "(loads previous valid hash if no file given)\n" \
103
- '(command can be combined' \
104
- " using '#{ARGS_FROM_HASH}'\n" \
105
- " with other commands which have '{}' arguments)") <<
106
- usage_line('persist', '',
107
- 'Toggles persistent mode' \
108
- "(whether is internal hash\n" \
109
- 'not invalidated after use)') <<
110
- usage_line('quit', '',
111
- "Shuts the client down gracefully\n" \
112
- '(SIGINT and EOF also work)', 'q') <<
113
- "\n[<arg>] means optional argument <arg>" \
114
- "\n[<args...>] means multiple arguments <args...> or none" \
115
- "\n (<args...> requires at least one)" \
116
- "\n{<arg>} means that either <arg>" \
117
- " or '#{ARGS_FROM_HASH}' is expected" \
118
- "\n (when '#{ARGS_FROM_HASH}' specified as first argument," \
119
- ' internal hash is used' \
120
- "\n -> 'load' command is called with rest arguments)" \
121
- "\n ({} means that only internal hash can be used," \
122
- "\n '#{ARGS_FROM_HASH}' is not necessary in this case)"
123
- end
6
+ # Provides handling of concrete commands from shell command line.
7
+ module Commands
8
+ private
9
+
10
+ # List of commands and their aliases.
11
+ COMMANDS = {
12
+ 'usage' => %w[? h help],
13
+ 'data' => %w[B buffer d],
14
+ 'history' => %w[H hist],
15
+ 'clear' => %w[C],
16
+ 'dump' => %w[D],
17
+ 'list' => %w[l],
18
+ 'config' => %w[c],
19
+ 'send' => %w[cmd s set],
20
+ 'pair' => %w[n],
21
+ 'ntp' => %w[N f],
22
+ 'url' => %w[U u],
23
+ 'wake' => %w[w z],
24
+ 'metadata' => %w[m meta],
25
+ 'delete' => %w[del],
26
+ 'reset' => %w[],
27
+ 'verbose' => %w[V],
28
+ 'save' => %w[S],
29
+ 'load' => %w[L],
30
+ 'persist' => %w[P],
31
+ 'quit' => %w[q],
32
+ }.freeze
33
+
34
+ # Returns usage message for a single command.
35
+ # @param command [String] command key from {COMMANDS}.
36
+ # @param args [String] arguments accepted by command.
37
+ # @param description [String] description of command.
38
+ # @param message [String, nil] optional message type
39
+ # that is about to be sent by the command.
40
+ # @param response [String, nil] optional message type
41
+ # of expected response to this message.
42
+ # @return [String] usage message for +command+.
43
+ def usage_cmd(command, args, description,
44
+ message = nil, response = nil)
45
+ cmds_str = (COMMANDS[command].dup << command).join('|')
46
+ cmds_str << ' ' << args unless args.empty?
47
+
48
+ description, *rest = description.split("\n")
49
+ rest << "[#{message} message]" if message
50
+ rest << "[#{response} response]" if response
51
+ rest = if rest.empty?
52
+ ''
53
+ else
54
+ rest.map { |s| ' ' * 52 + s }.join("\n") << "\n"
55
+ end
56
+
57
+ ' ' << cmds_str << ' ' * (48 - cmds_str.size) <<
58
+ description << "\n" << rest
59
+ end
124
60
 
125
- def list_hashes(history)
126
- buffer(:recv, :hashes, history).each_with_index do |h, i|
127
- puts "<#{i + 1}>"
128
- print_hash(h)
129
- puts
61
+ # Prints usage message composed mainly
62
+ # from particular {#usage_cmd} calls.
63
+ def cmd_usage
64
+ puts "\nUSAGE: <command> [<arguments...>]\nCOMMADS:\n" <<
65
+ usage_cmd('usage', '',
66
+ 'Prints this message') <<
67
+ usage_cmd('data', '',
68
+ 'Lists buffered received data (hashes)') <<
69
+ usage_cmd('history', '',
70
+ 'Lists all received data incl. the cleared') <<
71
+ usage_cmd('clear', '',
72
+ "Clears collected data\n" \
73
+ '(resp. moves it to history)') <<
74
+ usage_cmd('dump', '',
75
+ "Shortcut for 'data' + 'clear'") <<
76
+ usage_cmd('list', '',
77
+ 'Requests for new list of devices', 'l', 'L') <<
78
+ usage_cmd('config', '',
79
+ 'Requests for configuration message', 'c', 'C') <<
80
+ usage_cmd('send', '{}',
81
+ 'Sends settings to connected devices',
82
+ 's', 'S') <<
83
+ usage_cmd('pair', '{<timeout>}',
84
+ 'Sets device into pairing mode' \
85
+ " with optional timeout\n" \
86
+ '(request for a new device)', 'n', 'N') <<
87
+ usage_cmd('ntp', '{<NTP servers...>}',
88
+ 'Requests for NTP servers' \
89
+ ' and optionally updates them',
90
+ 'f', 'F') <<
91
+ usage_cmd('url', '{<URL> <port>}',
92
+ 'Configures Cube\'s portal URL', 'u') <<
93
+ usage_cmd('wake', '{<time> <scope> [<ID>]}',
94
+ 'Wake-ups the Cube',
95
+ 'z', 'A') <<
96
+ usage_cmd('metadata', '{}',
97
+ 'Serializes metadata for the Cube',
98
+ 'm', 'M') <<
99
+ usage_cmd('delete', '{<count> <force> <RF addresses...>}',
100
+ 'Deletes one or more devices from the Cube (!)',
101
+ 't', 'A') <<
102
+ usage_cmd('reset', '',
103
+ 'Requests for factory reset (!)', 'a', 'A') <<
104
+ usage_cmd('verbose', '',
105
+ "Toggles verbose mode (whether is incoming data\n" \
106
+ 'printed immediately or is not printed)') <<
107
+ usage_cmd('save', '[a|A|all]',
108
+ "Saves buffered [all] received and sent data\n" \
109
+ "into files at '#{@save_data_dir}'") <<
110
+ usage_cmd('load', '[<path>]',
111
+ 'Loads first hash from YAML file' \
112
+ " to internal variable\n" \
113
+ "-> to pass data with outgoing message\n" \
114
+ 'If path is relative,' \
115
+ " it looks in '#{@load_data_dir}'\n" \
116
+ "(loads previous valid hash if no file given)\n" \
117
+ '(command can be combined' \
118
+ " using '#{ARGS_FROM_HASH}'\n" \
119
+ ' with other commands' \
120
+ " which have '{}' arguments)") <<
121
+ usage_cmd('persist', '',
122
+ 'Toggles persistent mode' \
123
+ "(whether is internal hash\n" \
124
+ 'not invalidated after use)') <<
125
+ usage_cmd('quit', '',
126
+ "Shuts the client down gracefully\n" \
127
+ '(SIGINT and EOF also work)', 'q') <<
128
+ "\n[<arg>] means optional argument <arg>" \
129
+ "\n[<args...>] means multiple arguments <args...> or none" \
130
+ "\n (<args...> requires at least one)" \
131
+ "\n{<arg>} means that either <arg>" \
132
+ " or '#{ARGS_FROM_HASH}' is expected" \
133
+ "\n (when '#{ARGS_FROM_HASH}' specified as first argument," \
134
+ ' internal hash is used' \
135
+ "\n -> 'load' command is called with rest arguments)" \
136
+ "\n ({} means that only internal hash can be used," \
137
+ "\n '#{ARGS_FROM_HASH}' is not necessary in this case)"
130
138
  end
131
- end
132
139
 
133
- def cmd_data
134
- list_hashes(false)
135
- end
140
+ # Displays received hashes optionally with history.
141
+ # Calls {#buffer}.
142
+ # @param history [Boolean] whether to include history.
143
+ def list_hashes(history)
144
+ buffer(:recv, :hashes, history).each_with_index do |h, i|
145
+ puts "<#{i + 1}>"
146
+ print_hash(h)
147
+ puts
148
+ end
149
+ end
136
150
 
137
- def cmd_history
138
- list_hashes(true)
139
- end
151
+ # Calls {#list_hashes} without history.
152
+ def cmd_data
153
+ list_hashes(false)
154
+ end
140
155
 
141
- def cmd_clear
142
- %i[data hashes].each do |sym|
143
- @history[:recv][sym] += @buffer[:recv][sym]
144
- @buffer[:recv][sym].clear
156
+ # Calls {#list_hashes} with history.
157
+ def cmd_history
158
+ list_hashes(true)
145
159
  end
146
- end
147
160
 
148
- def cmd_dump
149
- cmd_data
150
- cmd_clear
151
- end
161
+ # Clears all buffered received data
162
+ # (i.e. moves them to history).
163
+ def cmd_clear
164
+ %i[data hashes].each do |sym|
165
+ @history[:recv][sym] += @buffer[:recv][sym]
166
+ @buffer[:recv][sym].clear
167
+ end
168
+ end
152
169
 
153
- def cmd_list
154
- send_msg('l')
155
- end
170
+ # Calls {#cmd_data} and {#cmd_clear}.
171
+ def cmd_dump
172
+ cmd_data
173
+ cmd_clear
174
+ end
156
175
 
157
- def cmd_config
158
- send_msg('c')
159
- end
176
+ # Calls {#send_msg} with message type 'l'
177
+ # (device list request).
178
+ def cmd_list
179
+ send_msg('l')
180
+ end
160
181
 
161
- def cmd_send(*args)
162
- send_msg('s', *args, load_only: true)
163
- end
182
+ # Calls {#send_msg} with message type 'c'
183
+ # (configuration message request).
184
+ def cmd_config
185
+ send_msg('c')
186
+ end
164
187
 
165
- def cmd_pair(*args)
166
- send_msg('n', *args)
167
- end
188
+ # Calls {#send_msg} with message type 's'
189
+ # (send command message)
190
+ # and +args+ and +load_only+ option.
191
+ # @param args [Array<String>] arguments from command line.
192
+ def cmd_send(*args)
193
+ send_msg('s', *args, load_only: true)
194
+ end
168
195
 
169
- def cmd_url(*args)
170
- send_msg('u', *args)
171
- end
196
+ # Calls {#send_msg} with message type 'n'
197
+ # (new device request)
198
+ # and +args+.
199
+ # @param args [Array<String>] arguments from command line.
200
+ def cmd_pair(*args)
201
+ send_msg('n', *args)
202
+ end
172
203
 
173
- def cmd_ntp(*args)
174
- send_msg('f', *args, last_array: true)
175
- end
204
+ # Calls {#send_msg} with message type 'u'
205
+ # (set portal URL message)
206
+ # and +args+.
207
+ # @param args [Array<String>] arguments from command line.
208
+ def cmd_url(*args)
209
+ send_msg('u', *args)
210
+ end
176
211
 
177
- def cmd_wake(*args)
178
- send_msg('z', *args)
179
- end
212
+ # Calls {#send_msg} with message type 'f'
213
+ # (NTP servers message)
214
+ # and +args+ and +last_array+ option.
215
+ # @param args [Array<String>] arguments from command line.
216
+ def cmd_ntp(*args)
217
+ send_msg('f', *args, last_array: true)
218
+ end
180
219
 
181
- def cmd_metadata(*args)
182
- send_msg('m', *args, load_only: true)
183
- end
220
+ # Calls {#send_msg} with message type 'z'
221
+ # (wake-up message)
222
+ # and +args+.
223
+ # @param args [Array<String>] arguments from command line.
224
+ def cmd_wake(*args)
225
+ send_msg('z', *args)
226
+ end
184
227
 
185
- def cmd_delete(*args)
186
- send_msg('t', *args, last_array: true, array_nonempty: true)
187
- end
228
+ # Calls {#send_msg} with message type 'm'
229
+ # (metadata message)
230
+ # and +args+ and +load_only+ option.
231
+ # @param args [Array<String>] arguments from command line.
232
+ def cmd_metadata(*args)
233
+ send_msg('m', *args, load_only: true)
234
+ end
188
235
 
189
- def cmd_reset
190
- send_msg('a')
191
- end
236
+ # Calls {#send_msg} with message type 't'
237
+ # (delete a device request)
238
+ # and +args+, and +last_array+ and +array_nonempty+ options.
239
+ # @param args [Array<String>] arguments from command line.
240
+ def cmd_delete(*args)
241
+ send_msg('t', *args, last_array: true, array_nonempty: true)
242
+ end
192
243
 
193
- def toggle(name, flag)
194
- puts "#{name}: #{flag} -> #{!flag}"
195
- !flag
196
- end
244
+ # Calls {#send_msg} with message type 'a'
245
+ # (factory reset request).
246
+ def cmd_reset
247
+ send_msg('a')
248
+ end
197
249
 
198
- def cmd_verbose
199
- @verbose = toggle('verbose', @verbose)
200
- end
250
+ def cmd_save(what = nil)
251
+ buffer = !what
252
+ all = %w[a A all].include?(what)
253
+ unless all || buffer
254
+ puts "Unrecognized argument: '#{what}'"
255
+ return
256
+ end
201
257
 
202
- def cmd_save(what = nil)
203
- buffer = !what
204
- all = %w[a A all].include?(what)
205
- unless all || buffer
206
- puts "Unrecognized argument: '#{what}'"
207
- return
208
- end
258
+ dir = @save_data_dir + Time.now.strftime('%Y%m%d-%H%M')
259
+ dir.mkpath
209
260
 
210
- dir = @save_data_dir + Time.now.strftime('%Y%m%d-%H%M')
211
- dir.mkpath
261
+ %i[recv sent].each do |sym|
262
+ data_fn = dir + (sym.to_s << '.data')
263
+ File.open(data_fn, 'w') do |f|
264
+ f.puts(buffer(sym, :data, all).join)
265
+ end
212
266
 
213
- %i[recv sent].each do |sym|
214
- data_fn = dir + (sym.to_s << '.data')
215
- File.open(data_fn, 'w') do |f|
216
- f.puts(buffer(sym, :data, all).join)
267
+ hashes_fn = dir + (sym.to_s << '.yaml')
268
+ File.open(hashes_fn, 'w') do |f|
269
+ buffer(sym, :hashes, all).to_yaml(f)
270
+ end
217
271
  end
218
272
 
219
- hashes_fn = dir + (sym.to_s << '.yaml')
220
- File.open(hashes_fn, 'w') do |f|
221
- buffer(sym, :hashes, all).to_yaml(f)
222
- end
273
+ which = buffer ? 'Buffered' : 'All'
274
+ puts "#{which} received and sent raw data and hashes" \
275
+ " saved into '#{dir}'"
276
+ rescue SystemCallError => e
277
+ puts "Files could not been saved:\n#{e}"
223
278
  end
224
279
 
225
- which = buffer ? 'Buffered' : 'All'
226
- puts "#{which} received and sent raw data and hashes" \
227
- " saved into '#{dir}'"
228
- rescue SystemCallError => e
229
- puts "Files could not been saved:\n#{e}"
230
- end
280
+ def parse_hash(path)
281
+ unless path.file? && path.readable?
282
+ return puts "File is not readable: '#{path}'"
283
+ end
231
284
 
232
- def parse_hash(path)
233
- unless path.file? && path.readable?
234
- return puts "File is not readable: '#{path}'"
285
+ hash = YAML.load_file(path)
286
+ hash = hash.first while hash.is_a?(Array)
287
+ raise YAML::SyntaxError unless hash.is_a?(Hash)
288
+ hash
289
+ rescue YAML::SyntaxError => e
290
+ puts "File '#{path}' does not contain proper YAML hash", e
235
291
  end
236
292
 
237
- hash = YAML.load_file(path)
238
- hash = hash.first while hash.is_a?(Array)
239
- raise YAML::SyntaxError unless hash.is_a?(Hash)
240
- hash
241
- rescue YAML::SyntaxError => e
242
- puts "File '#{path}' does not contain proper YAML hash", e
243
- end
293
+ def load_hash(path = nil)
294
+ if path
295
+ path = Pathname.new(path)
296
+ path = @load_data_dir + path if path.relative?
297
+ return parse_hash(path)
298
+ end
299
+ return @hash if @hash && @hash_set
244
300
 
245
- def load_hash(path = nil)
246
- if path
247
- path = Pathname.new(path)
248
- path = @load_data_dir + path if path.relative?
249
- return parse_hash(path)
301
+ if @hash
302
+ puts 'Internal hash is not set'
303
+ else
304
+ puts 'No internal hash loaded yet'
305
+ cmd_usage
306
+ end
250
307
  end
251
- return @hash if @hash && @hash_set
252
308
 
253
- if @hash
254
- puts 'Internal hash is not set'
255
- else
256
- puts 'No internal hash loaded yet'
257
- cmd_usage
309
+ def assign_hash(hash)
310
+ valid_hash = !hash.nil?
311
+ @hash = hash if valid_hash
312
+ @hash_set |= valid_hash
313
+ valid_hash
258
314
  end
259
- end
260
315
 
261
- def assign_hash(hash)
262
- valid_hash = !hash.nil?
263
- @hash = hash if valid_hash
264
- @hash_set |= valid_hash
265
- valid_hash
266
- end
316
+ def cmd_load(path = nil)
317
+ hash = load_hash(path)
318
+ return false unless assign_hash(hash)
319
+ print_hash(hash)
320
+ true
321
+ end
267
322
 
268
- def cmd_load(path = nil)
269
- hash = load_hash(path)
270
- return false unless assign_hash(hash)
271
- print_hash(hash)
272
- true
273
- end
323
+ # Helper method that 'toggles' boolean value and prints context info.
324
+ # (It actually does not modify +flag+.)
325
+ # @param name [String] variable name.
326
+ # @param flag [Boolean] boolean value to be 'toggled'.
327
+ # @return [Boolean] negated value of +flag+.
328
+ def toggle(name, flag)
329
+ puts "#{name}: #{flag} -> #{!flag}"
330
+ !flag
331
+ end
274
332
 
275
- def cmd_persist
276
- @persist = toggle('persist', @persist)
277
- @hash_set = @persist if @hash
278
- end
333
+ # Calls {#toggle} with verbose mode variable.
334
+ def cmd_verbose
335
+ @verbose = toggle('verbose', @verbose)
336
+ end
279
337
 
280
- def cmd_quit
281
- raise Interrupt
338
+ # Calls {#toggle} with persist mode variable.
339
+ def cmd_persist
340
+ @persist = toggle('persist', @persist)
341
+ @hash_set = @persist if @hash
342
+ end
343
+
344
+ # Manages to close the client gracefully
345
+ # (it should lead to {#close}).
346
+ def cmd_quit
347
+ raise Interrupt
348
+ end
282
349
  end
283
350
  end
284
351
  end