ruby_home 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -5
  3. data/bin/rubyhome +4 -4
  4. data/lib/ruby_home/accessory_info.rb +45 -30
  5. data/lib/ruby_home/factories/characteristic_factory.rb +27 -19
  6. data/lib/ruby_home/factories/default_values/float_value.rb +1 -1
  7. data/lib/ruby_home/factories/service_factory.rb +89 -0
  8. data/lib/ruby_home/factories/templates/characteristic_template.rb +6 -2
  9. data/lib/ruby_home/hap/accessory.rb +24 -3
  10. data/lib/ruby_home/hap/accessory_collection.rb +38 -0
  11. data/lib/ruby_home/hap/characteristic.rb +25 -19
  12. data/lib/ruby_home/hap/crypto/session_key.rb +31 -0
  13. data/lib/ruby_home/hap/ev_response.rb +64 -0
  14. data/lib/ruby_home/{http → hap}/hap_request.rb +12 -5
  15. data/lib/ruby_home/{http → hap}/hap_response.rb +7 -4
  16. data/lib/ruby_home/hap/server.rb +81 -0
  17. data/lib/ruby_home/hap/service.rb +11 -15
  18. data/lib/ruby_home/http/application.rb +17 -22
  19. data/lib/ruby_home/http/controllers/application_controller.rb +8 -4
  20. data/lib/ruby_home/http/controllers/characteristics_controller.rb +19 -4
  21. data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +3 -5
  22. data/lib/ruby_home/http/serializers/object_serializer.rb +1 -1
  23. data/lib/ruby_home/http/services/socket_notifier.rb +40 -0
  24. data/lib/ruby_home/http/services/start_srp_service.rb +36 -34
  25. data/lib/ruby_home/http/services/verify_srp_service.rb +55 -53
  26. data/lib/ruby_home/identifier_cache.rb +22 -49
  27. data/lib/ruby_home/persistable.rb +36 -0
  28. data/lib/ruby_home/version.rb +1 -1
  29. data/lib/ruby_home.rb +14 -5
  30. data/rubyhome.gemspec +3 -3
  31. metadata +35 -38
  32. data/lib/ruby_home/factories/accessory_factory.rb +0 -73
  33. data/lib/ruby_home/http/hap_server.rb +0 -60
  34. data/lib/ruby_home/rack/handler/hap_server.rb +0 -21
  35. data/lib/ruby_home/yaml_record.rb +0 -440
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf4e167745548086679350014c0375cf64eeb8733b9e6948ef0846ea36fd4f11
4
- data.tar.gz: d8583372ac979eceafcd3c6a1cf36f8f40c32d5ab543ff28b7cf03e11e294d9b
3
+ metadata.gz: b7f5f53b947e2cfbce79204572dd8d29b284c5434304aee7214b6a7a33d3e0a6
4
+ data.tar.gz: '086845819574df45f838c8ff67979f55f01763b6fe44114af76fd3479f8f1b83'
5
5
  SHA512:
6
- metadata.gz: 84a6186ceb1d259b60bfe877ee3e12c78c4d9a25cb3fc9433605c45264a3bc174796650bb87e95795f8c39ea6dd365bc22425c6360636e07923f731cacaf185d
7
- data.tar.gz: 19899a408055f4dd74993b03679cee97ea4d440b09beccbf6aaf4d034db727a96d09a73fc85ddb85f3fc4326704782ec861f9e006149a05c9e6c00c57123513d
6
+ metadata.gz: 419df1283d71010c0a4e8e89f60f01fa752ea9366f6fe59a1ca3a2e202a9ab72361ed4dff5389ecdfb8b084feb40cd0d5c62fcbf16b90b65bfcd5096c7a3a4ab
7
+ data.tar.gz: 3644d107b711edadd639f881614ebd85cb8e164cbeebf5db30e1748303983b52bf056371e952a6c9c0e39aa2a7744351549ddaf2eef4c655428dabdf0c9fe2eb
data/README.md CHANGED
@@ -21,18 +21,18 @@ Or install it yourself as:
21
21
 
22
22
  $ gem install ruby_home
23
23
 
24
- ## Usage
24
+ ## Basic Usage
25
25
 
26
26
  Create a fan with an on/off switch.
27
27
 
28
28
  ```ruby
29
29
  require 'ruby_home'
30
30
 
31
- accessory_information = RubyHome::AccessoryFactory.create(:accessory_information)
32
- fan = RubyHome::AccessoryFactory.create(:fan)
31
+ accessory_information = RubyHome::ServiceFactory.create(:accessory_information)
32
+ fan = RubyHome::ServiceFactory.create(:fan)
33
33
 
34
- fan.characteristic(:on).on(:updated) do |new_value|
35
- if new_value == 1
34
+ fan.characteristic(:on).after_update do |characteristic|
35
+ if characteristic.value == 1
36
36
  puts "Fan switched on"
37
37
  else
38
38
  puts "Fan switched off"
@@ -42,6 +42,65 @@ end
42
42
  RubyHome.run
43
43
  ```
44
44
 
45
+ Create a garage door opener.
46
+
47
+ ```ruby
48
+ require 'ruby_home'
49
+
50
+ accessory_information = RubyHome::ServiceFactory.create(:accessory_information)
51
+ door = RubyHome::ServiceFactory.create(:garage_door_opener)
52
+
53
+ door.characteristic(:target_door_state).after_update do |characteristic|
54
+ if characteristic.value == 0 # open
55
+ sleep 1
56
+ door.characteristic(:current_door_state).value = 0
57
+ elsif characteristic.value == 1 #closed
58
+ sleep 1
59
+ door.characteristic(:current_door_state).value = 1
60
+ end
61
+ end
62
+
63
+ RubyHome.run
64
+ ```
65
+
66
+ ## Customization
67
+
68
+ RubyHome ties to provide sane defaults for all services. Customization of any of the options is possible.
69
+
70
+ ```ruby
71
+ require 'ruby_home'
72
+
73
+ accessory_information = RubyHome::ServiceFactory.create(:accessory_information,
74
+ firmware_revision: '4.3.18421',
75
+ manufacturer: 'Fake Company',
76
+ model: 'BSB001',
77
+ name: 'Kickass fan bridge',
78
+ serial_number: 'AB1-UK-A123456'
79
+ )
80
+
81
+ fan = RubyHome::ServiceFactory.create(:fan,
82
+ on: false,
83
+ rotation_speed: 50,
84
+ rotation_direction: 1,
85
+ firmware_revision: '105.0.21169',
86
+ manufacturer: 'Fake Company',
87
+ model: 'LWB006',
88
+ name: 'Kickass fan',
89
+ serial_number: '123-UK-A12345'
90
+ )
91
+
92
+ fan.characteristic(:on).after_update do |characteristic|
93
+ if characteristic.value == 1
94
+ puts "Fan switched on"
95
+ else
96
+ puts "Fan switched off"
97
+ end
98
+ end
99
+
100
+ RubyHome.run
101
+ ```
102
+
103
+
45
104
  ## Development
46
105
 
47
106
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/bin/rubyhome CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  require_relative '../lib/ruby_home'
4
4
 
5
- accessory_information = RubyHome::AccessoryFactory.create(:accessory_information)
6
- fan = RubyHome::AccessoryFactory.create(:fan)
5
+ accessory_information = RubyHome::ServiceFactory.create(:accessory_information)
6
+ fan = RubyHome::ServiceFactory.create(:fan)
7
7
 
8
- fan.characteristic(:on).on(:updated) do |new_value|
9
- if new_value == 1
8
+ fan.characteristic(:on).after_update do |characteristic|
9
+ if characteristic.value == 1
10
10
  puts "Fan switched on"
11
11
  else
12
12
  puts "Fan switched off"
@@ -1,24 +1,46 @@
1
- require_relative 'device_id'
2
- require_relative 'yaml_record'
1
+ require_relative 'persistable'
3
2
 
4
3
  module RubyHome
5
- class AccessoryInfo < YamlRecord::Base
4
+ class AccessoryInfo
5
+ include Persistable
6
+
7
+ self.source = 'accessory_info.yml'
8
+
9
+ def self.instance
10
+ @@_instance ||= persisted || create
11
+ end
12
+
13
+ def self.reload
14
+ @@_instance = nil
15
+ end
16
+
6
17
  USERNAME = -'Pair-Setup'
7
18
 
8
- properties :device_id, :paired_clients, :password, :signature_key
9
- source 'accessory_info.yml'
19
+ def initialize(device_id: nil, paired_clients: [], password: nil, signature_key: nil)
20
+ @device_id = device_id
21
+ @paired_clients = paired_clients
22
+ @password = password
23
+ @signature_key = signature_key
24
+ end
10
25
 
11
- set_callback :before_create, :set_device_id
12
- set_callback :before_create, :set_paired_clients
13
- set_callback :before_create, :set_password
14
- set_callback :before_create, :set_signature_key
26
+ def username
27
+ USERNAME
28
+ end
15
29
 
16
- def self.instance
17
- first || create
30
+ def password
31
+ @password ||= Password.generate
32
+ end
33
+
34
+ def device_id
35
+ @device_id ||= DeviceID.generate
36
+ end
37
+
38
+ def paired_clients
39
+ @paired_clients ||= []
18
40
  end
19
41
 
20
42
  def add_paired_client(admin: false, identifier: , public_key: )
21
- paired_clients << { admin: admin, identifier: identifier, public_key: public_key }
43
+ @paired_clients << { admin: admin, identifier: identifier, public_key: public_key }
22
44
  save
23
45
  end
24
46
 
@@ -35,26 +57,19 @@ module RubyHome
35
57
  @signing_key ||= RbNaCl::Signatures::Ed25519::SigningKey.new([signature_key].pack('H*'))
36
58
  end
37
59
 
38
- def username
39
- USERNAME
40
- end
41
-
42
60
  private
43
61
 
44
- def set_device_id
45
- self.device_id ||= DeviceID.generate
46
- end
47
-
48
- def set_paired_clients
49
- self.paired_clients = []
50
- end
62
+ def signature_key
63
+ @signature_key ||= RbNaCl::Signatures::Ed25519::SigningKey.generate.to_bytes.unpack1('H*')
64
+ end
51
65
 
52
- def set_password
53
- self.password ||= Password.generate
54
- end
55
-
56
- def set_signature_key
57
- self.signature_key ||= RbNaCl::Signatures::Ed25519::SigningKey.generate.to_bytes.unpack1('H*')
58
- end
66
+ def persisted_attributes
67
+ {
68
+ device_id: device_id,
69
+ paired_clients: paired_clients,
70
+ password: password,
71
+ signature_key: signature_key
72
+ }
73
+ end
59
74
  end
60
75
  end
@@ -4,37 +4,45 @@ module RubyHome
4
4
  identify: nil,
5
5
  }.freeze
6
6
 
7
- def self.create(characteristic_name, options={}, &block)
8
- new(characteristic_name, options).create(&block)
9
- end
10
-
11
- def initialize(characteristic_name, options)
12
- @characteristic_name = characteristic_name
13
- @options = options
7
+ def self.create(characteristic_name, service: , value: nil)
8
+ new(
9
+ characteristic_name: characteristic_name,
10
+ service: service,
11
+ value: value
12
+ ).create
14
13
  end
15
14
 
16
15
  def create
17
- yield characteristic if block_given?
16
+ service.characteristics << new_characteristic
18
17
 
19
- characteristic.save
20
- characteristic
18
+ new_characteristic
21
19
  end
22
20
 
23
21
  private
24
22
 
25
- attr_reader :characteristic_name, :options
26
-
27
- def template
28
- @template ||= CharacteristicTemplate.find_by(name: characteristic_name.to_sym)
23
+ def initialize(characteristic_name:, service:, value:)
24
+ @characteristic_name = characteristic_name.to_sym
25
+ @service = service
26
+ @value = value || default_value
29
27
  end
30
28
 
31
- def characteristic
32
- @characteristic ||= Characteristic.new(characteristic_params)
29
+ attr_reader :service, :characteristic_name, :value
30
+
31
+ def new_characteristic
32
+ @new_characteristic ||= Characteristic.new(
33
+ description: template.description,
34
+ format: template.format,
35
+ name: characteristic_name,
36
+ properties: template.properties,
37
+ service: service,
38
+ unit: template.unit,
39
+ uuid: template.uuid,
40
+ value: value
41
+ )
33
42
  end
34
43
 
35
- def characteristic_params
36
- options[:value] ||= default_value
37
- options.merge(template.to_hash)
44
+ def template
45
+ @template ||= CharacteristicTemplate.find_by(name: characteristic_name)
38
46
  end
39
47
 
40
48
  def default_value
@@ -9,7 +9,7 @@ module RubyHome
9
9
  private
10
10
 
11
11
  def minimum_value
12
- template.constraints.fetch('MinimumValue')
12
+ template.constraints.fetch('MinimumValue', 0)
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,89 @@
1
+ module RubyHome
2
+ class ServiceFactory
3
+ def self.create(service_name, accessory: Accessory.new, **options)
4
+ new(
5
+ service_name: service_name,
6
+ accessory: accessory,
7
+ **options
8
+ ).create
9
+ end
10
+
11
+ def create
12
+ accessory.services << new_service
13
+
14
+ create_accessory_information
15
+ create_required_characteristics
16
+ create_optional_characteristics
17
+
18
+ new_service
19
+ end
20
+
21
+ private
22
+ ACCESSORY_INFORMATION_OPTIONS = [
23
+ :firmware_revision,
24
+ :manufacturer,
25
+ :model,
26
+ :name,
27
+ :serial_number
28
+ ].freeze
29
+
30
+ def initialize(service_name:, accessory:, **options)
31
+ @service_name = service_name.to_sym
32
+ @accessory = accessory
33
+ @options = options
34
+ end
35
+
36
+ attr_reader :service_name, :accessory, :options
37
+
38
+ def new_service
39
+ @new_service ||= Service.new(
40
+ accessory: accessory,
41
+ description: template.description,
42
+ name: service_name,
43
+ uuid: template.uuid
44
+ )
45
+ end
46
+
47
+ def create_required_characteristics
48
+ template.required_characteristics.each do |characteristic_template|
49
+ characteristic_name = characteristic_template.name
50
+ CharacteristicFactory.create(
51
+ characteristic_template.name,
52
+ service: new_service,
53
+ value: options[characteristic_name]
54
+ )
55
+ end
56
+ end
57
+
58
+ def create_optional_characteristics
59
+ optional_characteristic_templates.each do |characteristic_template|
60
+ characteristic_name = characteristic_template.name
61
+ CharacteristicFactory.create(
62
+ characteristic_name,
63
+ service: new_service,
64
+ value: options[characteristic_name]
65
+ )
66
+ end
67
+ end
68
+
69
+ def optional_characteristic_templates
70
+ template.optional_characteristics.select do |characteristic_template|
71
+ options.keys.include?(characteristic_template.name)
72
+ end
73
+ end
74
+
75
+ def create_accessory_information
76
+ unless service_name == :accessory_information
77
+ ServiceFactory.create(
78
+ :accessory_information,
79
+ accessory: accessory,
80
+ **options.slice(*ACCESSORY_INFORMATION_OPTIONS)
81
+ )
82
+ end
83
+ end
84
+
85
+ def template
86
+ @template ||= ServiceTemplate.find_by(name: service_name)
87
+ end
88
+ end
89
+ end
@@ -15,7 +15,7 @@ module RubyHome
15
15
  end
16
16
  end
17
17
 
18
- def initialize(name:, description:, uuid:, format:, unit:, permissions:, properties:, constraints:)
18
+ def initialize(name:, description:, uuid:, format:, unit:, permissions:, properties:, constraints: )
19
19
  @name = name
20
20
  @description = description
21
21
  @uuid = uuid
@@ -26,7 +26,11 @@ module RubyHome
26
26
  @constraints = constraints
27
27
  end
28
28
 
29
- attr_reader :name, :description, :uuid, :format, :unit, :permissions, :properties, :constraints
29
+ attr_reader :name, :description, :uuid, :format, :unit, :permissions, :properties
30
+
31
+ def constraints
32
+ @constraints || {}
33
+ end
30
34
 
31
35
  def to_hash
32
36
  {
@@ -1,11 +1,22 @@
1
1
  module RubyHome
2
2
  class Accessory
3
+ @@all = AccessoryCollection.new
4
+
5
+ def self.all
6
+ @@all
7
+ end
8
+
9
+ def self.reset
10
+ @@all = AccessoryCollection.new
11
+ end
12
+
3
13
  def initialize
4
14
  @services = []
15
+ @id = next_available_accessory_id
16
+ @@all << self
5
17
  end
6
18
 
7
- attr_accessor :id
8
- attr_reader :services
19
+ attr_reader :services, :id
9
20
 
10
21
  def characteristics
11
22
  services.flat_map(&:characteristics)
@@ -15,12 +26,22 @@ module RubyHome
15
26
  (largest_instance_id || 0) + 1
16
27
  end
17
28
 
29
+ private
30
+
18
31
  def instance_ids
19
- services.map(&:instance_id) + characteristics.map(&:instance_id)
32
+ instances.map(&:instance_id)
33
+ end
34
+
35
+ def instances
36
+ services + characteristics
20
37
  end
21
38
 
22
39
  def largest_instance_id
23
40
  instance_ids.max
24
41
  end
42
+
43
+ def next_available_accessory_id
44
+ self.class.all.count + 1
45
+ end
25
46
  end
26
47
  end
@@ -0,0 +1,38 @@
1
+ module RubyHome
2
+ class AccessoryCollection
3
+ include Enumerable
4
+ attr_accessor :accessories
5
+
6
+ def initialize(*accessories)
7
+ @accessories = accessories
8
+ end
9
+
10
+ def each
11
+ @accessories.map do |accessory|
12
+ yield accessory
13
+ end
14
+ end
15
+
16
+ def <<(accessory)
17
+ @accessories << accessory
18
+ end
19
+
20
+ def find_characteristic(attributes)
21
+ characteristics.find do |characteristic|
22
+ attributes.all? do |key, value|
23
+ characteristic.send(key) == value
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def services
31
+ accessories.flat_map(&:services)
32
+ end
33
+
34
+ def characteristics
35
+ services.flat_map(&:characteristics)
36
+ end
37
+ end
38
+ end
@@ -2,6 +2,18 @@ module RubyHome
2
2
  class Characteristic
3
3
  include Wisper::Publisher
4
4
 
5
+ public :local_registrations
6
+
7
+ def unsubscribe(*listeners)
8
+ local_registrations.delete_if do |registration|
9
+ listeners.include?(registration.listener)
10
+ end
11
+ end
12
+
13
+ def after_update(&block)
14
+ on(:after_update, &block)
15
+ end
16
+
5
17
  PROPERTIES = {
6
18
  'cnotify' => 'ev',
7
19
  'read' => 'pr',
@@ -18,10 +30,20 @@ module RubyHome
18
30
  @properties = properties
19
31
  @service = service
20
32
  @value = value
33
+ @instance_id = accessory.next_available_instance_id
21
34
  end
22
35
 
23
- attr_reader :service, :value, :uuid, :name, :description, :format, :unit, :properties
24
- attr_accessor :instance_id
36
+ attr_reader(
37
+ :service,
38
+ :value,
39
+ :uuid,
40
+ :name,
41
+ :description,
42
+ :format,
43
+ :unit,
44
+ :properties,
45
+ :instance_id
46
+ )
25
47
 
26
48
  def accessory
27
49
  service.accessory
@@ -37,24 +59,8 @@ module RubyHome
37
59
 
38
60
  def value=(new_value)
39
61
  return if name == :identify
40
-
41
62
  @value = new_value
42
- broadcast(:updated, new_value)
43
- end
44
-
45
- def inspect
46
- {
47
- name: name,
48
- value: value,
49
- accessory_id: accessory_id,
50
- service_iid: service_iid,
51
- instance_id: instance_id
52
- }
53
- end
54
-
55
- def save
56
- IdentifierCache.add_accessory(accessory)
57
- IdentifierCache.add_characteristic(self)
63
+ broadcast(:after_update, self)
58
64
  end
59
65
  end
60
66
  end
@@ -0,0 +1,31 @@
1
+ module RubyHome
2
+ module HAP
3
+ module Crypto
4
+ class SessionKey
5
+ def initialize(shared_secret)
6
+ @shared_secret = shared_secret
7
+ end
8
+
9
+ def controller_to_accessory_key
10
+ @controller_to_accessory_key ||= generate_shared_secret_key(WRITE)
11
+ end
12
+
13
+ def accessory_to_controller_key
14
+ @accessory_to_controller_key ||= generate_shared_secret_key(READ)
15
+ end
16
+
17
+ private
18
+
19
+ SALT = -'Control-Salt'
20
+ READ = -'Control-Read-Encryption-Key'
21
+ WRITE = -'Control-Write-Encryption-Key'
22
+
23
+ attr_reader :shared_secret
24
+
25
+ def generate_shared_secret_key(info)
26
+ Crypto::HKDF.new(info: info, salt: SALT).encrypt(shared_secret)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+ module RubyHome
2
+ module HAP
3
+ class EVResponse
4
+ def initialize(socket, body)
5
+ @socket = socket
6
+ @body = body
7
+ cache[:accessory_to_controller_count] ||= 0
8
+ end
9
+
10
+ def send_response
11
+ response = ''
12
+
13
+ send_header(response)
14
+ send_body(response)
15
+
16
+ encrypted_response = encrypter.encrypt(response).join
17
+ cache[:accessory_to_controller_count] = encrypter.count
18
+
19
+ socket << encrypted_response
20
+ end
21
+
22
+ private
23
+
24
+ CRLF = -"\x0d\x0a"
25
+ STATUS_LINE = -'EVENT/1.0 200 OK'
26
+ CONTENT_TYPE_LINE = -'Content-Type: application/hap+json'
27
+
28
+ def send_header(socket)
29
+ socket << STATUS_LINE + CRLF
30
+ socket << CONTENT_TYPE_LINE + CRLF
31
+ socket << content_length_line + CRLF
32
+ socket << CRLF
33
+ end
34
+
35
+ def send_body(socket)
36
+ socket << body
37
+ end
38
+
39
+ def content_length_line
40
+ "Content-Length: #{body.length}"
41
+ end
42
+
43
+ attr_reader :body, :socket
44
+
45
+ def encrypter
46
+ @_encrypter ||= RubyHome::HAP::HTTPEncryption.new(encryption_key, encrypter_params)
47
+ end
48
+
49
+ def encrypter_params
50
+ {
51
+ count: cache[:accessory_to_controller_count]
52
+ }
53
+ end
54
+
55
+ def encryption_key
56
+ cache[:accessory_to_controller_key]
57
+ end
58
+
59
+ def cache
60
+ RubyHome.socket_store[socket]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,13 +1,14 @@
1
1
  module RubyHome
2
- module HTTP
2
+ module HAP
3
3
  class HAPRequest < WEBrick::HTTPRequest
4
- def initialize(*args)
4
+ def initialize(config, sock)
5
+ @sock = sock
5
6
  cache[:controller_to_accessory_count] ||= 0
6
7
 
7
- super(*args)
8
+ super(config)
8
9
  end
9
10
 
10
- def parse(socket=nil)
11
+ def parse(socket)
11
12
  if decryption_time?
12
13
  request_line = socket.read_nonblock(@buffer_size)
13
14
 
@@ -24,8 +25,14 @@ module RubyHome
24
25
  cache[:controller_to_accessory_count] >= 1
25
26
  end
26
27
 
28
+ def meta_vars
29
+ super.merge({"REQUEST_SOCKET" => sock})
30
+ end
31
+
27
32
  private
28
33
 
34
+ attr_reader :sock
35
+
29
36
  def decrypter
30
37
  @_decrypter ||= RubyHome::HAP::HTTPDecryption.new(decryption_key, decrypter_params)
31
38
  end
@@ -45,7 +52,7 @@ module RubyHome
45
52
  end
46
53
 
47
54
  def cache
48
- RequestStore.store
55
+ RubyHome.socket_store[sock]
49
56
  end
50
57
  end
51
58
  end