cosmos 5.0.4 → 5.0.5

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.
@@ -32,6 +32,7 @@ require 'cosmos/models/tool_model'
32
32
  require 'cosmos/models/widget_model'
33
33
  require 'cosmos/models/microservice_model'
34
34
  require 'tmpdir'
35
+ require 'tempfile'
35
36
 
36
37
  module Cosmos
37
38
  # Represents a COSMOS plugin that can consist of targets, interfaces, routers
@@ -41,6 +42,9 @@ module Cosmos
41
42
  PRIMARY_KEY = 'cosmos_plugins'
42
43
 
43
44
  attr_accessor :variables
45
+ attr_accessor :plugin_txt_lines
46
+ attr_accessor :needs_dependencies
47
+
44
48
 
45
49
  # NOTE: The following three class methods are used by the ModelController
46
50
  # and are reimplemented to enable various Model class methods to work
@@ -58,71 +62,100 @@ module Cosmos
58
62
 
59
63
  # Called by the PluginsController to parse the plugin variables
60
64
  # Doesn't actaully create the plugin during the phase
61
- def self.install_phase1(gem_file_path, existing_variables = nil, scope:)
62
- gem_filename = File.basename(gem_file_path)
63
-
64
- # Load gem to internal gem server
65
- Cosmos::GemModel.put(gem_file_path, gem_install: false, scope: scope)
66
-
67
- # Extract gem and process plugin.txt to determine what VARIABLEs need to be filled in
68
- pkg = Gem::Package.new(gem_file_path)
65
+ def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, process_existing: false, scope:, validate_only: false)
66
+ gem_name = File.basename(gem_file_path).split("__")[0]
69
67
 
70
68
  temp_dir = Dir.mktmpdir
69
+ tf = nil
71
70
  begin
72
- pkg.extract_files(temp_dir)
73
- plugin_txt_path = File.join(temp_dir, 'plugin.txt')
74
- if File.exist?(plugin_txt_path)
75
- parser = Cosmos::ConfigParser.new("http://cosmosc2.com")
76
-
77
- # Phase 1 Gather Variables
78
- variables = {}
79
- parser.parse_file(plugin_txt_path,
80
- false,
81
- true,
82
- false) do |keyword, params|
83
- case keyword
84
- when 'VARIABLE'
85
- usage = "#{keyword} <Variable Name> <Default Value>"
86
- parser.verify_num_parameters(2, nil, usage)
87
- variable_name = params[0]
88
- value = params[1..-1].join(" ")
89
- variables[variable_name] = value
90
- if existing_variables && existing_variables.key?(variable_name)
91
- variables[variable_name] = existing_variables[variable_name]
92
- end
93
- # Ignore everything else during phase 1
94
- end
71
+ if File.exists?(gem_file_path)
72
+ # Load gem to internal gem server
73
+ Cosmos::GemModel.put(gem_file_path, gem_install: false, scope: scope) unless validate_only
74
+ else
75
+ gem_file_path = Cosmos::GemModel.get(temp_dir, gem_name)
76
+ end
77
+
78
+ # Extract gem and process plugin.txt to determine what VARIABLEs need to be filled in
79
+ pkg = Gem::Package.new(gem_file_path)
80
+
81
+ if existing_plugin_txt_lines and process_existing
82
+ # This is only used in cosmos load when everything is known
83
+ plugin_txt_lines = existing_plugin_txt_lines
84
+ file_data = existing_plugin_txt_lines.join("\n")
85
+ tf = Tempfile.new("plugin.txt")
86
+ tf.write(file_data)
87
+ tf.close
88
+ plugin_txt_path = tf.path
89
+ else
90
+ # Otherwise we always process the new and return both
91
+ pkg.extract_files(temp_dir)
92
+ plugin_txt_path = File.join(temp_dir, 'plugin.txt')
93
+ plugin_text = File.read(plugin_txt_path)
94
+ plugin_txt_lines = []
95
+ plugin_text.each_line do |line|
96
+ plugin_txt_lines << line.chomp
95
97
  end
98
+ end
99
+
100
+ parser = Cosmos::ConfigParser.new("http://cosmosc2.com")
96
101
 
97
- model = PluginModel.new(name: gem_filename, variables: variables, scope: scope)
98
- return model.as_json
102
+ # Phase 1 Gather Variables
103
+ variables = {}
104
+ parser.parse_file(plugin_txt_path,
105
+ false,
106
+ true,
107
+ false) do |keyword, params|
108
+ case keyword
109
+ when 'VARIABLE'
110
+ usage = "#{keyword} <Variable Name> <Default Value>"
111
+ parser.verify_num_parameters(2, nil, usage)
112
+ variable_name = params[0]
113
+ value = params[1..-1].join(" ")
114
+ variables[variable_name] = value
115
+ if existing_variables && existing_variables.key?(variable_name)
116
+ variables[variable_name] = existing_variables[variable_name]
117
+ end
118
+ # Ignore everything else during phase 1
119
+ end
99
120
  end
121
+
122
+ model = PluginModel.new(name: gem_name, variables: variables, plugin_txt_lines: plugin_txt_lines, scope: scope)
123
+ result = model.as_json
124
+ result['existing_plugin_txt_lines'] = existing_plugin_txt_lines if existing_plugin_txt_lines and not process_existing and existing_plugin_txt_lines != result['plugin_txt_lines']
125
+ return result
100
126
  ensure
101
127
  FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
128
+ tf.unlink if tf
102
129
  end
103
130
  end
104
131
 
105
132
  # Called by the PluginsController to create the plugin
106
133
  # Because this uses ERB it must be run in a seperate process from the API to
107
134
  # prevent corruption and single require problems in the current proces
108
- def self.install_phase2(name, variables, scope:)
135
+ def self.install_phase2(plugin_hash, scope:, validate_only: false)
109
136
  rubys3_client = Aws::S3::Client.new
110
137
 
111
138
  # Ensure config bucket exists
112
- begin
113
- rubys3_client.head_bucket(bucket: 'config')
114
- rescue Aws::S3::Errors::NotFound
115
- rubys3_client.create_bucket(bucket: 'config')
139
+ unless validate_only
140
+ begin
141
+ rubys3_client.head_bucket(bucket: 'config')
142
+ rescue Aws::S3::Errors::NotFound
143
+ rubys3_client.create_bucket(bucket: 'config')
144
+ end
116
145
  end
117
146
 
118
147
  # Register plugin to aid in uninstall if install fails
119
- plugin_model = PluginModel.new(name: name, variables: variables, scope: scope)
120
- plugin_model.create
148
+ plugin_hash.delete("existing_plugin_txt_lines")
149
+ plugin_model = PluginModel.new(**(plugin_hash.transform_keys(&:to_sym)), scope: scope)
150
+ plugin_model.create unless validate_only
121
151
 
122
152
  temp_dir = Dir.mktmpdir
123
153
  begin
154
+ tf = nil
155
+
124
156
  # Get the gem from local gem server
125
- gem_file_path = Cosmos::GemModel.get(temp_dir, name)
157
+ gem_name = plugin_hash['name'].split("__")[0]
158
+ gem_file_path = Cosmos::GemModel.get(temp_dir, gem_name)
126
159
 
127
160
  # Actually install the gem now (slow)
128
161
  Cosmos::GemModel.install(gem_file_path, scope: scope)
@@ -133,6 +166,11 @@ module Cosmos
133
166
  pkg = Gem::Package.new(gem_file_path)
134
167
  needs_dependencies = pkg.spec.runtime_dependencies.length > 0
135
168
  pkg.extract_files(gem_path)
169
+ needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
170
+ if needs_dependencies
171
+ plugin_model.needs_dependencies = true
172
+ plugin_model.update
173
+ end
136
174
 
137
175
  # Temporarily add all lib folders from the gem to the end of the load path
138
176
  load_dirs = []
@@ -145,7 +183,12 @@ module Cosmos
145
183
  end
146
184
 
147
185
  # Process plugin.txt file
148
- plugin_txt_path = File.join(gem_path, 'plugin.txt')
186
+ file_data = plugin_hash['plugin_txt_lines'].join("\n")
187
+ tf = Tempfile.new("plugin.txt")
188
+ tf.write(file_data)
189
+ tf.close
190
+ plugin_txt_path = tf.path
191
+ variables = plugin_hash['variables']
149
192
  if File.exist?(plugin_txt_path)
150
193
  parser = Cosmos::ConfigParser.new("http://cosmosc2.com")
151
194
 
@@ -156,8 +199,8 @@ module Cosmos
156
199
  # Ignore during phase 2
157
200
  when 'TARGET', 'INTERFACE', 'ROUTER', 'MICROSERVICE', 'TOOL', 'WIDGET'
158
201
  if current_model
159
- current_model.create
160
- current_model.deploy(gem_path, variables)
202
+ current_model.create unless validate_only
203
+ current_model.deploy(gem_path, variables, validate_only: validate_only)
161
204
  current_model = nil
162
205
  end
163
206
  current_model = Cosmos.const_get((keyword.capitalize + 'Model').intern).handle_config(parser, keyword, params, plugin: plugin_model.name, needs_dependencies: needs_dependencies, scope: scope)
@@ -170,8 +213,8 @@ module Cosmos
170
213
  end
171
214
  end
172
215
  if current_model
173
- current_model.create
174
- current_model.deploy(gem_path, variables)
216
+ current_model.create unless validate_only
217
+ current_model.deploy(gem_path, variables, validate_only: validate_only)
175
218
  current_model = nil
176
219
  end
177
220
  end
@@ -182,21 +225,28 @@ module Cosmos
182
225
  end
183
226
  rescue => err
184
227
  # Install failed - need to cleanup
185
- plugin_model.destroy
228
+ plugin_model.destroy unless validate_only
186
229
  raise err
187
230
  ensure
188
231
  FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
232
+ tf.unlink if tf
189
233
  end
234
+
235
+ return plugin_model.as_json
190
236
  end
191
237
 
192
238
  def initialize(
193
239
  name:,
194
240
  variables: {},
241
+ plugin_txt_lines: [],
242
+ needs_dependencies: false,
195
243
  updated_at: nil,
196
244
  scope:
197
245
  )
198
246
  super("#{scope}__#{PRIMARY_KEY}", name: name, updated_at: updated_at, scope: scope)
199
247
  @variables = variables
248
+ @plugin_txt_lines = plugin_txt_lines
249
+ @needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
200
250
  end
201
251
 
202
252
  def create(update: false, force: false)
@@ -208,6 +258,8 @@ module Cosmos
208
258
  {
209
259
  'name' => @name,
210
260
  'variables' => @variables,
261
+ 'plugin_txt_lines' => @plugin_txt_lines,
262
+ 'needs_dependencies' => @needs_dependencies,
211
263
  'updated_at' => @updated_at
212
264
  }
213
265
  end
@@ -220,5 +272,13 @@ module Cosmos
220
272
  end
221
273
  end
222
274
  end
275
+
276
+ # Reinstall
277
+ def restore
278
+ plugin_hash = self.as_json
279
+ plugin_hash['name'] = plugin_hash['name'].split("__")[0]
280
+ Cosmos::PluginModel.install_phase2(plugin_hash, scope: @scope)
281
+ @destroyed = false
282
+ end
223
283
  end
224
284
  end
@@ -52,6 +52,8 @@ module Cosmos
52
52
  def deploy(gem_path, variables)
53
53
  seed_database()
54
54
 
55
+ ConfigTopic.initialize_stream(@scope)
56
+
55
57
  # COSMOS Log Microservice
56
58
  microservice_name = "#{@scope}__COSMOS__LOG"
57
59
  microservice = MicroserviceModel.new(
@@ -119,6 +119,7 @@ module Cosmos
119
119
  def create(update: false)
120
120
  validate_start(update: update)
121
121
  @updated_at = Time.now.to_nsec_from_epoch
122
+ SortedModel.destroy(scope: @scope, start: update) if update
122
123
  Store.zadd(@primary_key, @start, JSON.generate(as_json()))
123
124
  if update
124
125
  notify(kind: 'updated')
@@ -129,8 +130,9 @@ module Cosmos
129
130
 
130
131
  # Update the Redis hash at primary_key
131
132
  def update(start:)
133
+ orig_start = @start
132
134
  @start = start
133
- create(update: true)
135
+ create(update: orig_start)
134
136
  end
135
137
 
136
138
  # destroy the activity from the redis database
@@ -22,6 +22,7 @@ require 'cosmos/models/model'
22
22
  require 'cosmos/models/cvt_model'
23
23
  require 'cosmos/models/microservice_model'
24
24
  require 'cosmos/topics/limits_event_topic'
25
+ require 'cosmos/topics/config_topic'
25
26
  require 'cosmos/system'
26
27
  require 'cosmos/utilities/s3'
27
28
  require 'cosmos/utilities/zip'
@@ -360,7 +361,7 @@ module Cosmos
360
361
  return nil
361
362
  end
362
363
 
363
- def deploy(gem_path, variables)
364
+ def deploy(gem_path, variables, validate_only: false)
364
365
  rubys3_client = Aws::S3::Client.new
365
366
  variables["target_name"] = @name
366
367
  start_path = "/targets/#{@folder_name}/"
@@ -389,14 +390,23 @@ module Cosmos
389
390
  FileUtils.mkdir_p(File.dirname(local_path))
390
391
  File.open(local_path, 'wb') { |file| file.write(data) }
391
392
  found = true
392
- rubys3_client.put_object(bucket: 'config', key: key, body: data)
393
+ rubys3_client.put_object(bucket: 'config', key: key, body: data) unless validate_only
393
394
  end
394
395
  raise "No target files found at #{target_path}" unless found
395
396
 
396
397
  target_folder = File.join(temp_dir, @name)
397
- build_target_archive(rubys3_client, temp_dir, target_folder)
398
- system = update_store(temp_dir)
399
- deploy_microservices(gem_path, variables, system)
398
+ # Build a System for just this target
399
+ system = System.new([@name], temp_dir)
400
+ if variables["xtce_output"]
401
+ puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
402
+ system.packet_config.to_xtce(variables["xtce_output"])
403
+ end
404
+ unless validate_only
405
+ build_target_archive(rubys3_client, temp_dir, target_folder)
406
+ system = update_store(system)
407
+ deploy_microservices(gem_path, variables, system)
408
+ ConfigTopic.write({ kind: 'created', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
409
+ end
400
410
  ensure
401
411
  FileUtils.remove_entry(temp_dir) if temp_dir and File.exist?(temp_dir)
402
412
  end
@@ -430,6 +440,7 @@ module Cosmos
430
440
  model = MicroserviceModel.get_model(name: "#{@scope}__#{type}__#{@name}", scope: @scope)
431
441
  model.destroy if model
432
442
  end
443
+ ConfigTopic.write({ kind: 'deleted', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
433
444
  end
434
445
 
435
446
  ##################################################
@@ -497,9 +508,7 @@ module Cosmos
497
508
  end
498
509
  end
499
510
 
500
- def update_store(temp_dir)
501
- # Build a System for just this target
502
- system = System.new([@name], temp_dir)
511
+ def update_store(system)
503
512
  target = system.targets[@name]
504
513
 
505
514
  # Add in the information from the target and update
@@ -606,7 +615,7 @@ module Cosmos
606
615
  ],
607
616
  topics: command_topic_list,
608
617
  target_names: [@name],
609
- plugin: plugin,
618
+ plugin: @plugin,
610
619
  needs_dependencies: @needs_dependencies,
611
620
  scope: @scope
612
621
  )
@@ -629,7 +638,7 @@ module Cosmos
629
638
  ],
630
639
  topics: decom_command_topic_list,
631
640
  target_names: [@name],
632
- plugin: plugin,
641
+ plugin: @plugin,
633
642
  needs_dependencies: @needs_dependencies,
634
643
  scope: @scope
635
644
  )
@@ -654,7 +663,7 @@ module Cosmos
654
663
  ],
655
664
  topics: packet_topic_list,
656
665
  target_names: [@name],
657
- plugin: plugin,
666
+ plugin: @plugin,
658
667
  needs_dependencies: @needs_dependencies,
659
668
  scope: @scope
660
669
  )
@@ -677,7 +686,7 @@ module Cosmos
677
686
  ],
678
687
  topics: decom_topic_list,
679
688
  target_names: [@name],
680
- plugin: plugin,
689
+ plugin: @plugin,
681
690
  needs_dependencies: @needs_dependencies,
682
691
  scope: @scope
683
692
  )
@@ -694,7 +703,7 @@ module Cosmos
694
703
  work_dir: '/cosmos/lib/cosmos/microservices',
695
704
  topics: packet_topic_list,
696
705
  target_names: [@name],
697
- plugin: plugin,
706
+ plugin: @plugin,
698
707
  needs_dependencies: @needs_dependencies,
699
708
  scope: @scope
700
709
  )
@@ -710,7 +719,7 @@ module Cosmos
710
719
  cmd: ["ruby", "reducer_microservice.rb", microservice_name],
711
720
  work_dir: '/cosmos/lib/cosmos/microservices',
712
721
  topics: decom_topic_list,
713
- plugin: plugin,
722
+ plugin: @plugin,
714
723
  needs_dependencies: @needs_dependencies,
715
724
  scope: @scope
716
725
  )
@@ -727,7 +736,7 @@ module Cosmos
727
736
  name: microservice_name,
728
737
  cmd: ["ruby", "cleanup_microservice.rb", microservice_name],
729
738
  work_dir: '/cosmos/lib/cosmos/microservices',
730
- plugin: plugin,
739
+ plugin: @plugin,
731
740
  scope: @scope
732
741
  )
733
742
  microservice.create
@@ -209,13 +209,11 @@ module Cosmos
209
209
  return nil
210
210
  end
211
211
 
212
- def deploy(gem_path, variables)
212
+ def deploy(gem_path, variables, validate_only: false)
213
213
  return unless @folder_name
214
214
 
215
- rubys3_client = Aws::S3::Client.new
216
-
217
215
  # Ensure tools bucket exists
218
- Cosmos::S3Utilities.ensure_public_bucket('tools')
216
+ Cosmos::S3Utilities.ensure_public_bucket('tools') unless validate_only
219
217
 
220
218
  variables["tool_name"] = @name
221
219
  start_path = "/tools/#{@folder_name}/"
@@ -223,16 +221,17 @@ module Cosmos
223
221
  next if filename == '.' or filename == '..' or File.directory?(filename)
224
222
 
225
223
  key = filename.split(gem_path + '/tools/')[-1]
226
-
227
224
  extension = filename.split('.')[-1]
228
225
  content_type = Rack::Mime.mime_type(".#{extension}")
229
226
 
230
- cache_control = Cosmos::S3Utilities.get_cache_control(filename)
231
-
232
227
  # Load tool files
233
228
  data = File.read(filename, mode: "rb")
234
229
  data = ERB.new(data, trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
235
- rubys3_client.put_object(bucket: 'tools', content_type: content_type, cache_control: cache_control, key: key, body: data)
230
+ unless validate_only
231
+ cache_control = Cosmos::S3Utilities.get_cache_control(filename)
232
+ Aws::S3::Client.new.put_object(bucket: 'tools', content_type: content_type, cache_control: cache_control, key: key, body: data)
233
+ ConfigTopic.write({ kind: 'created', type: 'tool', name: @folder_name, plugin: @plugin }, scope: @scope)
234
+ end
236
235
  end
237
236
  end
238
237
 
@@ -242,6 +241,7 @@ module Cosmos
242
241
  prefix = "#{@folder_name}/"
243
242
  rubys3_client.list_objects(bucket: 'tools', prefix: prefix).contents.each do |object|
244
243
  rubys3_client.delete_object(bucket: 'tools', key: object.key)
244
+ ConfigTopic.write({ kind: 'deleted', type: 'tool', name: @folder_name, plugin: @plugin }, scope: @scope)
245
245
  end
246
246
  end
247
247
  end
@@ -108,25 +108,25 @@ module Cosmos
108
108
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Widget: #{keyword} #{parameters.join(" ")}")
109
109
  end
110
110
 
111
- def deploy(gem_path, variables)
112
- rubys3_client = Aws::S3::Client.new
113
-
111
+ def deploy(gem_path, variables, validate_only: false)
114
112
  # Ensure tools bucket exists
115
- Cosmos::S3Utilities.ensure_public_bucket('tools')
113
+ Cosmos::S3Utilities.ensure_public_bucket('tools') unless validate_only
116
114
 
117
115
  filename = gem_path + "/tools/widgets/" + @full_name + '/' + @filename
118
116
 
119
- cache_control = Cosmos::S3Utilities.get_cache_control(@filename)
120
-
121
117
  # Load widget file
122
118
  data = File.read(filename, mode: "rb")
123
119
  Cosmos.set_working_dir(File.dirname(filename)) do
124
120
  data = ERB.new(data, trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
125
121
  end
126
- # TODO: support widgets that aren't just a single js file (and its associated map file)
127
- rubys3_client.put_object(bucket: 'tools', content_type: 'application/javascript', cache_control: cache_control, key: @s3_key, body: data)
128
- data = File.read(filename + '.map', mode: "rb")
129
- rubys3_client.put_object(bucket: 'tools', content_type: 'application/json', cache_control: cache_control, key: @s3_key + '.map', body: data)
122
+ unless validate_only
123
+ cache_control = Cosmos::S3Utilities.get_cache_control(@filename)
124
+ # TODO: support widgets that aren't just a single js file (and its associated map file)
125
+ rubys3_client = Aws::S3::Client.new
126
+ rubys3_client.put_object(bucket: 'tools', content_type: 'application/javascript', cache_control: cache_control, key: @s3_key, body: data)
127
+ data = File.read(filename + '.map', mode: "rb")
128
+ rubys3_client.put_object(bucket: 'tools', content_type: 'application/json', cache_control: cache_control, key: @s3_key + '.map', body: data)
129
+ end
130
130
  end
131
131
 
132
132
  def undeploy
@@ -98,7 +98,7 @@ module Cosmos
98
98
  # @param buffer [String] String buffer to hold the packet data
99
99
  # @param item_class [Class] Class used to instantiate items (Must be a
100
100
  # subclass of PacketItem)
101
- def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, description = nil, buffer = '', item_class = PacketItem)
101
+ def initialize(target_name, packet_name, default_endianness = :BIG_ENDIAN, description = nil, buffer = nil, item_class = PacketItem)
102
102
  super(default_endianness, buffer, item_class)
103
103
  # Explictly call the defined setter methods
104
104
  self.target_name = target_name
@@ -66,7 +66,7 @@ module Cosmos
66
66
  # @param buffer [String] Buffer used to store the structure
67
67
  # @param item_class [Class] Class used to instantiate new structure items.
68
68
  # Must be StructureItem or one of its subclasses.
69
- def initialize(default_endianness = BinaryAccessor::HOST_ENDIANNESS, buffer = '', item_class = StructureItem)
69
+ def initialize(default_endianness = BinaryAccessor::HOST_ENDIANNESS, buffer = nil, item_class = StructureItem)
70
70
  if (default_endianness == :BIG_ENDIAN) || (default_endianness == :LITTLE_ENDIAN)
71
71
  @default_endianness = default_endianness
72
72
  if buffer
@@ -102,14 +102,11 @@ module Cosmos
102
102
  def read_item(item, value_type = :RAW, buffer = @buffer)
103
103
  return nil if item.data_type == :DERIVED
104
104
 
105
- if buffer
106
- if item.array_size
107
- return BinaryAccessor.read_array(item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness)
108
- else
109
- return BinaryAccessor.read(item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness)
110
- end
105
+ buffer = allocate_buffer_if_needed() unless buffer
106
+ if item.array_size
107
+ return BinaryAccessor.read_array(item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness)
111
108
  else
112
- raise "No buffer given to read_item"
109
+ return BinaryAccessor.read(item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness)
113
110
  end
114
111
  end
115
112
 
@@ -117,9 +114,8 @@ module Cosmos
117
114
  #
118
115
  # @return [Integer] Size of the buffer in bytes
119
116
  def length
120
- return @buffer.length if @buffer
121
-
122
- return 0
117
+ allocate_buffer_if_needed()
118
+ return @buffer.length
123
119
  end
124
120
 
125
121
  # Resize the buffer at least the defined length of the structure
@@ -129,12 +125,23 @@ module Cosmos
129
125
  if @buffer.length < @defined_length
130
126
  @buffer << (ZERO_STRING * (@defined_length - @buffer.length))
131
127
  end
128
+ else
129
+ allocate_buffer_if_needed()
132
130
  end
133
131
 
134
132
  return self
135
133
  end
136
134
  end
137
135
 
136
+ # Allocate a buffer if not available
137
+ def allocate_buffer_if_needed
138
+ unless @buffer
139
+ @buffer = ZERO_STRING * @defined_length
140
+ @buffer.force_encoding(ASCII_8BIT_STRING)
141
+ end
142
+ return @buffer
143
+ end
144
+
138
145
  # Indicates if any items have been defined for this structure
139
146
  # @return [TrueClass or FalseClass]
140
147
  def defined?
@@ -340,14 +347,11 @@ module Cosmos
340
347
  # parameter to check whether to perform conversions on the item.
341
348
  # @param buffer [String] The binary buffer to write the value to
342
349
  def write_item(item, value, value_type = :RAW, buffer = @buffer)
343
- if buffer
344
- if item.array_size
345
- BinaryAccessor.write_array(value, item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness, item.overflow)
346
- else
347
- BinaryAccessor.write(value, item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness, item.overflow)
348
- end
350
+ buffer = allocate_buffer_if_needed() unless buffer
351
+ if item.array_size
352
+ BinaryAccessor.write_array(value, item.bit_offset, item.bit_size, item.data_type, item.array_size, buffer, item.endianness, item.overflow)
349
353
  else
350
- raise "No buffer given to write_item"
354
+ BinaryAccessor.write(value, item.bit_offset, item.bit_size, item.data_type, buffer, item.endianness, item.overflow)
351
355
  end
352
356
  end
353
357
 
@@ -432,14 +436,11 @@ module Cosmos
432
436
  # @param copy [TrueClass/FalseClass] Whether to copy the buffer
433
437
  # @return [String] Data buffer backing the structure
434
438
  def buffer(copy = true)
435
- if @buffer
436
- if copy
437
- return @buffer.dup
438
- else
439
- return @buffer
440
- end
439
+ local_buffer = allocate_buffer_if_needed()
440
+ if copy
441
+ return local_buffer.dup
441
442
  else
442
- return nil
443
+ return local_buffer
443
444
  end
444
445
  end
445
446
 
@@ -525,15 +526,11 @@ module Cosmos
525
526
  module MethodMissing
526
527
  # Method missing provides reading/writing item values as if they were methods to the class
527
528
  def method_missing(name, value = nil)
528
- if @buffer
529
- if value
530
- # Strip off the equals sign before looking up the item
531
- return write(name.to_s[0..-2], value)
532
- else
533
- return read(name.to_s)
534
- end
529
+ if value
530
+ # Strip off the equals sign before looking up the item
531
+ return write(name.to_s[0..-2], value)
535
532
  else
536
- raise "No buffer available for method_missing"
533
+ return read(name.to_s)
537
534
  end
538
535
  end
539
536
  end
@@ -42,6 +42,9 @@ module Cosmos
42
42
  # @param start [Time] Metadata time value, if nil will be current time
43
43
  # @return The result of the method call.
44
44
  def set_metadata(metadata, color: nil, start: nil)
45
+ unless metadata.is_a?(Hash)
46
+ raise "metadata must be a Hash: #{metadata} is a #{metadata.class}"
47
+ end
45
48
  color = color.nil? ? '#003784' : color
46
49
  data = { color: color, metadata: metadata }
47
50
  data[:start] = start.iso8601 unless start.nil?
@@ -52,12 +55,20 @@ module Cosmos
52
55
 
53
56
  # Updates the metadata
54
57
  #
55
- # @param start [Integer] Metadata time value as integer seconds from epoch
56
58
  # @param metadata [Hash<Symbol, Variable>] A hash of metadata
57
59
  # @param color [String] Events color to show on Calendar tool, if nil will be blue
60
+ # @param start [Integer] Metadata time value as integer seconds from epoch
58
61
  # @return The result of the method call.
59
- def update_metadata(start, metadata, color: nil)
62
+ def update_metadata(metadata, color: nil, start: nil)
63
+ unless metadata.is_a?(Hash)
64
+ raise "metadata must be a Hash: #{metadata} is a #{metadata.class}"
65
+ end
60
66
  color = color.nil? ? '#003784' : color
67
+ if start == nil
68
+ existing = get_metadata()
69
+ start = existing['start']
70
+ metadata = existing['metadata'].merge(metadata)
71
+ end
61
72
  data = { :color => color, :metadata => metadata }
62
73
  data[:start] = Time.at(start).iso8601
63
74
  response = $api_server.request('put', "/cosmos-api/metadata/#{start}", data: data, json: true)