somfy_sdn 1.0.12 → 2.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2c2926c376002fa9632188a5d467c64c2987e48f892975a1a725ec71f38d021
4
- data.tar.gz: dcafff75ded7c5e5cccc65912179dd6fbf0c33f36655cf29adb6e7e4d796b7a9
3
+ metadata.gz: aab121215242974fc0570c9495954b8a8584fd02535fe6b758590e3e5baa7a21
4
+ data.tar.gz: f2130a9042156d347808409bab26bec3dcdd37ac27d4a5fcd156ed90a889ed6a
5
5
  SHA512:
6
- metadata.gz: 6fc1a34073e8069bd778575a1e5f8b3531b2bf5a769f3010d2ca93d4c4a2af07af0e26c7e1bb7a7d094d729e41dfb7031a5d569294d285fc82ab5f2b93afeebe
7
- data.tar.gz: b61b3e69a01b4b0d709fa381ce0113184f6f5d32227ce3f72eee67c74bcf5e83ff8daa813ee69ef36e9fe76ffa9bc50c9d28eb76c403f1c1db370327ad505155
6
+ metadata.gz: a104e2ad0bba74257e89768c36ceba023208190074da12fe35420c451c981b9582521b6276fd342e6513d7949e7264b073c4975a66bbaacd4592577948ad3ed6
7
+ data.tar.gz: fce28168771816f6718d116fb0bc84daa02475b9a8318ddf65e16caa288c24303e2f57910d445d654c44b2dbb356ccf23b5b680b29120c3c9f751317ba661a6f
data/bin/somfy_sdn ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'somfy_sdn'
4
+ require 'thor'
5
+
6
+ class SomfySDNCLI < Thor
7
+ class_option :verbose, type: :boolean, default: false
8
+
9
+ desc "monitor PORT", "Monitor traffic on the SDN network at PORT"
10
+ def monitor(port)
11
+ handle_global_options
12
+
13
+ sdn = SDN::Client.new(port)
14
+
15
+ loop do
16
+ sdn.receive do |message|
17
+ SDN.logger.info "Received message #{message.inspect}"
18
+ end
19
+ end
20
+ end
21
+
22
+ desc "mqtt PORT MQTT_URI", "Run an MQTT bridge to control the SDN network at PORT"
23
+ option :"device-id", default: "somfy", desc: "The Homie Device ID"
24
+ option :"base-topic", default: "homie", desc: "The base Homie topic"
25
+ option :"auto-discover", type: :boolean, default: true, desc: "Do a discovery at startup"
26
+ def mqtt(port, mqtt_uri)
27
+ handle_global_options
28
+
29
+ require 'sdn/cli/mqtt'
30
+
31
+ SDN::CLI::MQTT.new(port, mqtt_uri,
32
+ device_id: options["device-id"],
33
+ base_topic: options["base-topic"],
34
+ auto_discover: options["auto-discover"])
35
+ end
36
+
37
+ desc "provision PORT [ADDRESS]", "Provision a motor (label and set limits) at PORT"
38
+ def provision(port, address = nil)
39
+ handle_global_options
40
+
41
+ require 'sdn/cli/provisioner'
42
+ SDN::CLI::Provisioner.new(port, address)
43
+ end
44
+
45
+ desc "simulator PORT [ADDRESS]", "Simulate a motor (for debugging purposes) at PORT"
46
+ def simulator(port, address = nil)
47
+ handle_global_options
48
+
49
+ require 'sdn/cli/simulator'
50
+ SDN::CLI::Simulator.new(port, address)
51
+ end
52
+
53
+ private
54
+
55
+ def handle_global_options
56
+ SDN.logger.level = options[:verbose] ? :debug : :info
57
+ end
58
+ end
59
+
60
+ SomfySDNCLI.start(ARGV)
@@ -0,0 +1,31 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ Group = Struct.new(:bridge, :addr, :position_percent, :position_pulses, :ip, :last_direction, :state, :motors) do
5
+ def initialize(*)
6
+ members.each { |k| self[k] = :nil }
7
+ super
8
+ end
9
+
10
+ def publish(attribute, value)
11
+ if self[attribute] != value
12
+ bridge.publish("#{addr}/#{attribute.to_s.gsub('_', '-')}", value.to_s)
13
+ self[attribute] = value
14
+ end
15
+ end
16
+
17
+ def printed_addr
18
+ Message.print_address(Message.parse_address(addr))
19
+ end
20
+
21
+ def motor_objects
22
+ bridge.motors.select { |addr, motor| motor.groups_string.include?(printed_addr) }.values
23
+ end
24
+
25
+ def motors_string
26
+ motor_objects.map { |m| Message.print_address(Message.parse_address(m.addr)) }.sort.join(',')
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,103 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ Motor = Struct.new(:bridge,
5
+ :addr,
6
+ :node_type,
7
+ :label,
8
+ :position_pulses,
9
+ :position_percent,
10
+ :ip,
11
+ :state,
12
+ :last_direction,
13
+ :last_action_source,
14
+ :last_action_cause,
15
+ :up_limit,
16
+ :down_limit,
17
+ :direction,
18
+ :up_speed,
19
+ :down_speed,
20
+ :slow_speed,
21
+ :ip1_pulses,
22
+ :ip1_percent,
23
+ :ip2_pulses,
24
+ :ip2_percent,
25
+ :ip3_pulses,
26
+ :ip3_percent,
27
+ :ip4_pulses,
28
+ :ip4_percent,
29
+ :ip5_pulses,
30
+ :ip5_percent,
31
+ :ip6_pulses,
32
+ :ip6_percent,
33
+ :ip7_pulses,
34
+ :ip7_percent,
35
+ :ip8_pulses,
36
+ :ip8_percent,
37
+ :ip9_pulses,
38
+ :ip9_percent,
39
+ :ip10_pulses,
40
+ :ip10_percent,
41
+ :ip11_pulses,
42
+ :ip11_percent,
43
+ :ip12_pulses,
44
+ :ip12_percent,
45
+ :ip13_pulses,
46
+ :ip13_percent,
47
+ :ip14_pulses,
48
+ :ip14_percent,
49
+ :ip15_pulses,
50
+ :ip15_percent,
51
+ :ip16_pulses,
52
+ :ip16_percent,
53
+ :groups,
54
+ :last_action,
55
+ :last_position_pulses) do
56
+ def initialize(*)
57
+ members.each { |k| self[k] = :nil }
58
+ @groups = [].fill(nil, 0, 16)
59
+ super
60
+ end
61
+
62
+ def publish(attribute, value)
63
+ if self[attribute] != value
64
+ bridge.publish("#{addr}/#{attribute.to_s.gsub('_', '-')}", value.to_s)
65
+ self[attribute] = value
66
+ end
67
+ end
68
+
69
+ def add_group(index, address)
70
+ group = bridge.add_group(Message.print_address(address)) if address
71
+ old_group = @groups[index - 1]
72
+ @groups[index - 1] = address
73
+ group&.publish(:motors, group.motors_string)
74
+ publish(:groups, groups_string)
75
+ bridge.touch_group(old_group) if old_group
76
+ end
77
+
78
+ def set_groups(groups)
79
+ return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
80
+ groups = groups.split(',').sort.uniq.map { |g| Message.parse_address(g) }.select { |g| Message.is_group_address?(g) }
81
+ groups.fill(nil, groups.length, 16 - groups.length)
82
+ messages = []
83
+ sdn_addr = Message.parse_address(addr)
84
+ groups.each_with_index do |g, i|
85
+ if @groups[i] != g
86
+ messages << Message::SetGroupAddr.new(sdn_addr, i + 1, g).tap { |m| m.ack_requested = true }
87
+ messages << Message::GetGroupAddr.new(sdn_addr, i + 1)
88
+ end
89
+ end
90
+ messages
91
+ end
92
+
93
+ def groups_string
94
+ @groups.compact.map { |g| Message.print_address(g) }.sort.uniq.join(',')
95
+ end
96
+
97
+ def group_objects
98
+ groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,156 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ module Read
5
+ def read
6
+ loop do
7
+ begin
8
+ @sdn.receive do |message|
9
+ @mqtt.batch_publish do
10
+ SDN.logger.info "read #{message.inspect}"
11
+
12
+ src = Message.print_address(message.src)
13
+ # ignore the UAI Plus and ourselves
14
+ if src != '7F.7F.7F' && !Message.is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
15
+ SDN.logger.info "found new motor #{src}"
16
+ @motors_found = true
17
+ motor = publish_motor(src.gsub('.', ''), message.node_type)
18
+ end
19
+
20
+ follow_ups = []
21
+ case message
22
+ when Message::PostNodeLabel
23
+ if (motor.publish(:label, message.label))
24
+ publish("#{motor.addr}/$name", message.label)
25
+ end
26
+ when Message::PostMotorPosition,
27
+ Message::ILT2::PostMotorPosition
28
+ if message.is_a?(Message::ILT2::PostMotorPosition)
29
+ # keep polling while it's still moving; check prior two positions
30
+ if motor.position_pulses == message.position_pulses &&
31
+ motor.last_position_pulses == message.position_pulses
32
+ motor.publish(:state, :stopped)
33
+ else
34
+ motor.publish(:state, :running)
35
+ if motor.position_pulses && motor.position_pulses != message.position_pulses
36
+ motor.publish(:last_direction, motor.position_pulses < message.position_pulses ? :down : :up)
37
+ end
38
+ follow_ups << Message::ILT2::GetMotorPosition.new(message.src)
39
+ end
40
+ motor.last_position_pulses = motor.position_pulses
41
+ ip = (1..16).find do |i|
42
+ # divide by 5 for some leniency
43
+ motor["ip#{i}_pulses"].to_i / 5 == message.position_pulses / 5
44
+ end
45
+ motor.publish(:ip, ip)
46
+ end
47
+ motor.publish(:position_percent, message.position_percent)
48
+ motor.publish(:position_pulses, message.position_pulses)
49
+ motor.publish(:ip, message.ip) if message.respond_to?(:ip)
50
+ motor.group_objects.each do |group|
51
+ positions_percent = group.motor_objects.map(&:position_percent)
52
+ positions_pulses = group.motor_objects.map(&:position_pulses)
53
+ ips = group.motor_objects.map(&:ip)
54
+
55
+ position_percent = nil
56
+ # calculate an average, but only if we know a position for
57
+ # every shade
58
+ if !positions_percent.include?(:nil) && !positions_percent.include?(nil)
59
+ position_percent = positions_percent.inject(&:+) / positions_percent.length
60
+ end
61
+
62
+ position_pulses = nil
63
+ if !positions_pulses.include?(:nil) && !positions_pulses.include?(nil)
64
+ position_pulses = positions_pulses.inject(&:+) / positions_pulses.length
65
+ end
66
+
67
+ ip = nil
68
+ ip = ips.first if ips.uniq.length == 1
69
+ ip = nil if ip == :nil
70
+
71
+ group.publish(:position_percent, position_percent)
72
+ group.publish(:position_pulses, position_pulses)
73
+ group.publish(:ip, ip)
74
+ end
75
+ when Message::PostMotorStatus
76
+ if message.state == :running || motor.state == :running ||
77
+ # if it's explicitly stopped, but we didn't ask it to, it's probably
78
+ # changing directions so keep querying
79
+ (message.state == :stopped &&
80
+ message.last_action_cause == :explicit_command &&
81
+ !(motor.last_action == Message::Stop || motor.last_action.nil?))
82
+ follow_ups << Message::GetMotorStatus.new(message.src)
83
+ end
84
+ # this will do one more position request after it stopped
85
+ follow_ups << Message::GetMotorPosition.new(message.src)
86
+ motor.publish(:state, message.state)
87
+ motor.publish(:last_direction, message.last_direction)
88
+ motor.publish(:last_action_source, message.last_action_source)
89
+ motor.publish(:last_action_cause, message.last_action_cause)
90
+ motor.group_objects.each do |group|
91
+ states = group.motor_objects.map(&:state).uniq
92
+ state = states.length == 1 ? states.first : 'mixed'
93
+ group.publish(:state, state)
94
+
95
+ directions = group.motor_objects.map(&:last_direction).uniq
96
+ direction = directions.length == 1 ? directions.first : 'mixed'
97
+ group.publish(:last_direction, direction)
98
+ end
99
+ when Message::PostMotorLimits
100
+ motor.publish(:up_limit, message.up_limit)
101
+ motor.publish(:down_limit, message.down_limit)
102
+ when Message::ILT2::PostMotorSettings
103
+ motor.publish(:down_limit, message.limit)
104
+ when Message::PostMotorDirection
105
+ motor.publish(:direction, message.direction)
106
+ when Message::PostMotorRollingSpeed
107
+ motor.publish(:up_speed, message.up_speed)
108
+ motor.publish(:down_speed, message.down_speed)
109
+ motor.publish(:slow_speed, message.slow_speed)
110
+ when Message::PostMotorIP,
111
+ Message::ILT2::PostMotorIP
112
+ motor.publish(:"ip#{message.ip}_pulses", message.position_pulses)
113
+ if message.respond_to?(:position_percent)
114
+ motor.publish(:"ip#{message.ip}_percent", message.position_percent)
115
+ elsif motor.down_limit
116
+ motor.publish(:"ip#{message.ip}_percent", message.position_pulses.to_f / motor.down_limit * 100)
117
+ end
118
+ when Message::PostGroupAddr
119
+ motor.add_group(message.group_index, message.group_address)
120
+ end
121
+
122
+ @mutex.synchronize do
123
+ prior_message_to_group = Message.is_group_address?(@prior_message&.message&.src) if @prior_message
124
+
125
+ correct_response = @response_pending && @prior_message&.message&.class&.expected_response?(message)
126
+ correct_response = false if !prior_message_to_group && message.src != @prior_message&.message&.dest
127
+ correct_response = false if prior_message_to_group && message.dest != @prior_message&.message&.src
128
+
129
+ if prior_message_to_group && correct_response
130
+ @pending_group_motors.delete(Message.print_address(message.src).gsub('.', ''))
131
+ correct_response = false unless @pending_group_motors.empty?
132
+ end
133
+
134
+ signal = correct_response || !follow_ups.empty?
135
+ @response_pending = @broadcast_pending if correct_response
136
+ follow_ups.each do |follow_up|
137
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
138
+ end
139
+ @cond.signal if signal
140
+ end
141
+ rescue EOFError
142
+ SDN.logger.fatal "EOF reading"
143
+ exit 2
144
+ rescue MalformedMessage => e
145
+ SDN.logger.warn "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
146
+ rescue => e
147
+ SDN.logger.error "got garbage: #{e}; #{e.backtrace}"
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,156 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ module Subscriptions
5
+ def handle_message(topic, value)
6
+ SDN.logger.info "got #{value.inspect} at #{topic}"
7
+ if (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{6})/(?<property>discover|label|control|jog-(?<jog_type>pulses|ms)|position-pulses|position-percent|ip|reset|(?<speed_type>up-speed|down-speed|slow-speed)|up-limit|down-limit|direction|ip(?<ip>\d+)-(?<ip_type>pulses|percent)|groups)/set$}))
8
+ addr = Message.parse_address(match[:addr])
9
+ property = match[:property]
10
+ # not homie compliant; allows linking the position-percent property
11
+ # directly to an OpenHAB rollershutter channel
12
+ if property == 'position-percent' && value =~ /^(?:UP|DOWN|STOP)$/i
13
+ property = "control"
14
+ value = value.downcase
15
+ end
16
+ mqtt_addr = Message.print_address(addr).gsub('.', '')
17
+ motor = @motors[mqtt_addr]
18
+ is_group = Message.is_group_address?(addr)
19
+ group = @groups[mqtt_addr]
20
+ follow_up = motor&.node_type == :st50ilt2 ? Message::ILT2::GetMotorPosition.new(addr) :
21
+ Message::GetMotorStatus.new(addr)
22
+ ns = motor&.node_type == :st50ilt2 ? Message::ILT2 : Message
23
+
24
+ message = case property
25
+ when 'discover'
26
+ follow_up = nil
27
+ if value == "discover"
28
+ # discovery is low priority, and longer timeout
29
+ enqueue(MessageAndRetries.new(Message::GetNodeAddr.new(addr), 1, 2), 2)
30
+ end
31
+ nil
32
+ when 'label'
33
+ follow_up = Message::GetNodeLabel.new(addr)
34
+ ns::SetNodeLabel.new(addr, value) unless is_group
35
+ when 'control'
36
+ case value
37
+ when 'up', 'down'
38
+ (motor&.node_type == :st50ilt2 ? ns::SetMotorPosition : Message::MoveTo).
39
+ new(addr, "#{value}_limit".to_sym)
40
+ when 'stop'
41
+ motor&.node_type == :st50ilt2 ? ns::SetMotorPosition.new(addr, :stop) : Message::Stop.new(addr)
42
+ when 'next_ip'
43
+ motor&.node_type == :st50ilt2 ? ns::SetMotorPosition.new(addr, :next_ip_down) :
44
+ Message::MoveOf.new(addr, :next_ip)
45
+ when 'previous_ip'
46
+ motor&.node_type == :st50ilt2 ? ns::SetMotorPosition.new(addr, :next_ip_up) :
47
+ Message::MoveOf.new(addr, :previous_ip)
48
+ when 'wink'
49
+ Message::Wink.new(addr)
50
+ when 'refresh'
51
+ follow_up = nil
52
+ (motor&.node_type == :st50ilt2 ? ns::GetMotorPosition : Message::GetMotorStatus).
53
+ new(addr)
54
+ end
55
+ when /jog-(?:pulses|ms)/
56
+ value = value.to_i
57
+ (motor&.node_type == :st50ilt2 ? ns::SetMotorPosition : Message::MoveOf).
58
+ new(addr, "jog_#{value < 0 ? :up : :down }_#{match[:jog_type]}".to_sym, value.abs)
59
+ when 'reset'
60
+ return unless Message::SetFactoryDefault::RESET.keys.include?(value.to_sym)
61
+ Message::SetFactoryDefault.new(addr, value.to_sym)
62
+ when 'position-pulses', 'position-percent', 'ip'
63
+ if value == 'REFRESH'
64
+ follow_up = nil
65
+ (motor&.node_type == :st50ilt2 ? ns::GetMotorPosition : Message::GetMotorStatus).
66
+ new(addr)
67
+ else
68
+ (motor&.node_type == :st50ilt2 ? ns::SetMotorPosition : Message::MoveTo).
69
+ new(addr, property.sub('position-', 'position_').to_sym, value.to_i)
70
+ end
71
+ when 'direction'
72
+ return if is_group
73
+ follow_up = Message::GetMotorDirection.new(addr)
74
+ return unless %w{standard reversed}.include?(value)
75
+ Message::SetMotorDirection.new(addr, value.to_sym)
76
+ when 'up-limit', 'down-limit'
77
+ return if is_group
78
+ if %w{delete current_position jog_ms jog_pulses}.include?(value)
79
+ type = value.to_sym
80
+ value = 10
81
+ else
82
+ type = :specified_position
83
+ end
84
+ target = property == 'up-limit' ? :up : :down
85
+ follow_up = Message::GetMotorLimits.new(addr)
86
+ Message::SetMotorLimits.new(addr, type, target, value.to_i)
87
+ when /^ip\d-(?:pulses|percent)$/
88
+ return if is_group
89
+ ip = match[:ip].to_i
90
+ return unless (1..16).include?(ip)
91
+ follow_up = ns::GetMotorIP.new(addr, ip)
92
+
93
+ if motor&.node_type == :st50ilt2
94
+ value = if value == 'delete'
95
+ nil
96
+ elsif value == 'current_position'
97
+ motor.position_pulses
98
+ elsif match[:ip_type] == 'pulses'
99
+ value.to_i
100
+ else
101
+ value.to_f / motor.down_limit * 100
102
+ end
103
+ ns::SetMotorIP.new(addr, ip, value)
104
+ else
105
+ type = if value == 'delete'
106
+ :delete
107
+ elsif value == 'current_position'
108
+ :current_position
109
+ elsif match[:ip_type] == 'pulses'
110
+ :position_pulses
111
+ else
112
+ :position_percent
113
+ end
114
+ Message::SetMotorIP.new(addr, type, ip, value.to_i)
115
+ end
116
+ when 'up-speed', 'down-speed', 'slow-speed'
117
+ return if is_group
118
+ return unless motor
119
+ follow_up = Message::GetMotorRollingSpeed.new(addr)
120
+ message = Message::SetMotorRollingSpeed.new(addr,
121
+ up_speed: motor.up_speed,
122
+ down_speed: motor.down_speed,
123
+ slow_speed: motor.slow_speed)
124
+ message.send(:"#{property.sub('-', '_')}=", value.to_i)
125
+ message
126
+ when 'groups'
127
+ return if is_group
128
+ return unless motor
129
+ messages = motor.set_groups(value)
130
+ @mutex.synchronize do
131
+ messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
132
+ @cond.signal
133
+ end
134
+ nil
135
+ end
136
+
137
+ if motor
138
+ motor.last_action = message.class if [Message::MoveTo, Message::Move, Message::Wink, Message::Stop].include?(message.class)
139
+ end
140
+
141
+ if message
142
+ message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
143
+ @mutex.synchronize do
144
+ @queues[0].push(MessageAndRetries.new(message, 5, 0))
145
+ if follow_up
146
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
147
+ end
148
+ @cond.signal
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,93 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ module Write
5
+ def write
6
+ last_write_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
+
8
+ loop do
9
+ message_and_retries = nil
10
+ @mutex.synchronize do
11
+ # got woken up early by another command getting queued; spin
12
+ if @response_pending
13
+ while @response_pending
14
+ remaining_wait = @response_pending - Time.now.to_f
15
+ if remaining_wait < 0
16
+ SDN.logger.debug "timed out waiting on response"
17
+ @response_pending = nil
18
+ @broadcast_pending = nil
19
+ if @prior_message && @prior_message&.remaining_retries != 0
20
+ SDN.logger.debug "retrying #{@prior_message.remaining_retries} more times ..."
21
+ if Message.is_group_address?(@prior_message.message.src) && !@pending_group_motors.empty?
22
+ SDN.logger.debug "re-targetting group message to individual motors"
23
+ @pending_group_motors.each do |addr|
24
+ new_message = @prior_message.message.dup
25
+ new_message.src = [0, 0, 1]
26
+ new_message.dest = Message.parse_address(addr)
27
+ @queues[@prior_message.priority].push(MessageAndRetries.new(new_message, @prior_message.remaining_retries, @prior_message.priority))
28
+ end
29
+ @pending_group_motors = []
30
+ else
31
+ @queues[@prior_message.priority].push(@prior_message)
32
+ end
33
+ @prior_message = nil
34
+ end
35
+ else
36
+ @cond.wait(@mutex, remaining_wait)
37
+ end
38
+ end
39
+ end
40
+
41
+ @queues.find { |q| message_and_retries = q.shift }
42
+ if message_and_retries
43
+ if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
44
+ @response_pending = Time.now.to_f + WAIT_TIME
45
+ @pending_group_motors = if Message.is_group_address?(message_and_retries.message.src)
46
+ group_addr = Message.print_address(message_and_retries.message.src).gsub('.', '')
47
+ @groups[group_addr]&.motor_objects&.map(&:addr) || []
48
+ else
49
+ []
50
+ end
51
+
52
+ if message_and_retries.message.dest == BROADCAST_ADDRESS || Message.is_group_address?(message_and_retries.message.src) && message_and_retries.message.is_a?(Message::GetNodeAddr)
53
+ @broadcast_pending = Time.now.to_f + BROADCAST_WAIT
54
+ end
55
+ end
56
+ end
57
+
58
+ # wait until there is a message
59
+ if @response_pending
60
+ message_and_retries.remaining_retries -= 1
61
+ @prior_message = message_and_retries
62
+ elsif message_and_retries
63
+ @prior_message = nil
64
+ else
65
+ if @auto_discover && @motors_found
66
+ # nothing pending to write, and motors found on the last iteration;
67
+ # look for more motors
68
+ message_and_retries = MessageAndRetries.new(Message::GetNodeAddr.new, 1, 2)
69
+ @motors_found = false
70
+ else
71
+ @cond.wait(@mutex)
72
+ end
73
+ end
74
+ end
75
+ next unless message_and_retries
76
+
77
+ message = message_and_retries.message
78
+ SDN.logger.info "writing #{message.inspect}"
79
+ # minimum time between messages
80
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ sleep_time = 0.1 - (now - last_write_at)
82
+ sleep(sleep_time) if sleep_time > 0
83
+ @sdn.send(message)
84
+ last_write_at = now
85
+ end
86
+ rescue => e
87
+ SDN.logger.fatal "failure writing: #{e}: #{e.backtrace}"
88
+ exit 1
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end