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 +4 -4
- data/bin/somfy_sdn +60 -0
- data/lib/sdn/cli/mqtt/group.rb +31 -0
- data/lib/sdn/cli/mqtt/motor.rb +103 -0
- data/lib/sdn/cli/mqtt/read.rb +156 -0
- data/lib/sdn/cli/mqtt/subscriptions.rb +156 -0
- data/lib/sdn/cli/mqtt/write.rb +93 -0
- data/lib/sdn/cli/mqtt.rb +397 -0
- data/lib/sdn/cli/provisioner.rb +234 -0
- data/lib/sdn/cli/simulator.rb +197 -0
- data/lib/sdn/client.rb +86 -0
- data/lib/sdn/{messages → message}/control.rb +79 -38
- data/lib/sdn/{messages → message}/get.rb +48 -40
- data/lib/sdn/{messages → message}/helpers.rb +33 -3
- data/lib/sdn/message/ilt2/get.rb +48 -0
- data/lib/sdn/message/ilt2/master_control.rb +35 -0
- data/lib/sdn/message/ilt2/post.rb +127 -0
- data/lib/sdn/message/ilt2/set.rb +192 -0
- data/lib/sdn/{messages → message}/post.rb +127 -61
- data/lib/sdn/{messages → message}/set.rb +99 -84
- data/lib/sdn/message.rb +81 -30
- data/lib/sdn/version.rb +1 -1
- data/lib/sdn.rb +16 -1
- metadata +63 -26
- data/bin/sdn_mqtt_bridge +0 -5
- data/lib/sdn/messages/ilt2/get.rb +0 -9
- data/lib/sdn/messages/ilt2/post.rb +0 -18
- data/lib/sdn/messages/ilt2/set.rb +0 -59
- data/lib/sdn/mqtt_bridge.rb +0 -711
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aab121215242974fc0570c9495954b8a8584fd02535fe6b758590e3e5baa7a21
|
4
|
+
data.tar.gz: f2130a9042156d347808409bab26bec3dcdd37ac27d4a5fcd156ed90a889ed6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|