somfy_sdn 1.0.12 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
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