adam6050 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +10 -2
- data/adam6050.gemspec +8 -1
- data/lib/adam6050/handler/login.rb +9 -1
- data/lib/adam6050/handler/read.rb +16 -1
- data/lib/adam6050/handler/status.rb +5 -1
- data/lib/adam6050/handler/write.rb +2 -1
- data/lib/adam6050/password.rb +10 -0
- data/lib/adam6050/server.rb +33 -2
- data/lib/adam6050/session.rb +22 -5
- data/lib/adam6050/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c831e1aa753967132fff58891039d680b8affbc
|
4
|
+
data.tar.gz: 27d7aad6b04f7085c8b059e744434fea7939ddb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aeba0631241b8976c771d32c62e1c9d9ed2d299e6d57b335c01529241e16daf449ee1004294f399d9ad3b2617ba4300baeea29e861c3ae4cff8ebeb6e4758013
|
7
|
+
data.tar.gz: 28e971677d1e7befbdec55f875e86f288c62266218a0694506bb17f72b133fe85379ab5eb989ff2512d489df2f72f9365c2f8f932d48164754876f010494b765
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
-
#
|
1
|
+
# ADAM6050
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/adam6050.svg)](https://badge.fury.io/rb/vissen-input)
|
4
4
|
[![Build Status](https://travis-ci.org/seblindberg/ruby-adam6050.svg?branch=master)](https://travis-ci.org/seblindberg/ruby-adam6050)
|
5
5
|
[![Inline docs](http://inch-ci.org/github/seblindberg/ruby-adam6050.svg?branch=master)](http://inch-ci.org/github/seblindberg/ruby-adam6050)
|
6
6
|
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/adam6050/)
|
7
7
|
|
8
|
-
|
8
|
+
![Advantech ADAM-6050 IO module](http://downloadt.advantech.com/download/downloadlit.aspx?LIT_ID=1-3150PW)
|
9
|
+
|
10
|
+
This library implements a server that emulates the functionality of the network connected Advantech ADAM-6050 digital IO module. Specifically the UDP protocol that the unit speaks has been reverse engineered. Since I don't have an actual device to test with the response messages from the server may differ from what they should be. It all works well enough for interfacing with Synology Surveillance Station which is the original intent.
|
11
|
+
|
12
|
+
More information about the module can be found on the Advantech [product page](http://www.advantech.com/products/a67f7853-013a-4b50-9b20-01798c56b090/adam-6050/mod_b009c4b4-4b7c-4736-b16f-241978245e6a) which among other things links to its manual.
|
9
13
|
|
10
14
|
## Installation
|
11
15
|
|
@@ -34,6 +38,10 @@ server.run do |state, prev_state|
|
|
34
38
|
end
|
35
39
|
```
|
36
40
|
|
41
|
+
## TODO
|
42
|
+
|
43
|
+
-[ ] Improve the reliability of the server tests that involve socket connections. Hard coded delays are no good.
|
44
|
+
|
37
45
|
## Development
|
38
46
|
|
39
47
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/adam6050.gemspec
CHANGED
@@ -12,7 +12,14 @@ Gem::Specification.new do |spec|
|
|
12
12
|
|
13
13
|
spec.summary = 'Server implementation of the ADAM-6050 IO module.'
|
14
14
|
spec.description = 'This library implements a server that emulates the ' \
|
15
|
-
'
|
15
|
+
'functionality of the network connected Advantech ' \
|
16
|
+
'ADAM-6050 digital IO module. Specifically the UDP ' \
|
17
|
+
'protocol that the unit speaks has been reverse ' \
|
18
|
+
"engineered. Since I don't have an actual device to " \
|
19
|
+
'test with the response messages from the server may ' \
|
20
|
+
'differ from what they should be. It all works well ' \
|
21
|
+
'enough for interfacing with Synology Surveillance ' \
|
22
|
+
'Station which is the original intent.'
|
16
23
|
spec.homepage = 'https://github.com/seblindberg/ruby-adam6050'
|
17
24
|
spec.license = 'MIT'
|
18
25
|
|
@@ -3,6 +3,10 @@
|
|
3
3
|
module ADAM6050
|
4
4
|
module Handler
|
5
5
|
# Allows senders to login.
|
6
|
+
#
|
7
|
+
# I have so far not been able to find any documentation around this feature.
|
8
|
+
# It is therefore almost certain that the response in case of an incorrect
|
9
|
+
# password is wrong.
|
6
10
|
class Login
|
7
11
|
include Handler
|
8
12
|
|
@@ -18,8 +22,12 @@ module ADAM6050
|
|
18
22
|
|
19
23
|
# @param msg [String] the incomming message.
|
20
24
|
# @param state [Integer] the current state.
|
25
|
+
# @param session [Session] the current session.
|
26
|
+
# @param sender [Socket::UDPSource] the UDP client.
|
21
27
|
#
|
22
|
-
# @return [
|
28
|
+
# @return [Integer] the next state (always unchanged).
|
29
|
+
# @return [String] a reply. The reply is either '>01' or '?' depending on
|
30
|
+
# if the login attempt was successful or not.
|
23
31
|
def handle(msg, state, session, sender)
|
24
32
|
return state, '?' unless @password == msg[6..-1].chomp!
|
25
33
|
|
@@ -3,6 +3,20 @@
|
|
3
3
|
module ADAM6050
|
4
4
|
module Handler
|
5
5
|
# Allows registed senders to read the state.
|
6
|
+
#
|
7
|
+
# From the manual:
|
8
|
+
# Name Read Channel Status
|
9
|
+
# Description This command requests that the specified ADAM-6000 module
|
10
|
+
# return the status of its digital input channels.
|
11
|
+
# Syntax #01C\r
|
12
|
+
# Response !0100(data)(data)(data)(data)\r
|
13
|
+
# (data) a 2-character hexadecimal value representing the
|
14
|
+
# values of the digital input module.
|
15
|
+
#
|
16
|
+
# TODO: The manual clearly states that onlyt the status of the input
|
17
|
+
# channels should be included in the response. There are however
|
18
|
+
# examples out there that also include the output.
|
19
|
+
#
|
6
20
|
class Read
|
7
21
|
include Handler
|
8
22
|
|
@@ -10,7 +24,8 @@ module ADAM6050
|
|
10
24
|
MESSAGE_PREAMBLE = '$016'
|
11
25
|
|
12
26
|
# @param state [Integer] the current state.
|
13
|
-
# @return [
|
27
|
+
# @return [Integer] the next state (always unchanged).
|
28
|
+
# @return [String] the reply.
|
14
29
|
def handle(_, state, *)
|
15
30
|
# From the manual:
|
16
31
|
# The first 2-character portion of the response (exclude the "!"
|
@@ -3,6 +3,9 @@
|
|
3
3
|
module ADAM6050
|
4
4
|
module Handler
|
5
5
|
# Allows registed senders to read the IO status.
|
6
|
+
#
|
7
|
+
# I have so far not been able to find any documentation around this feature.
|
8
|
+
# The meaning of the rely is therefore currently unknown.
|
6
9
|
class Status
|
7
10
|
include Handler
|
8
11
|
|
@@ -11,7 +14,8 @@ module ADAM6050
|
|
11
14
|
|
12
15
|
# @param msg [String] the incomming message.
|
13
16
|
# @param state [Integer] the current state.
|
14
|
-
# @return [
|
17
|
+
# @return [Integer] the next state (always unchanged).
|
18
|
+
# @return [String] the reply.
|
15
19
|
def handle(msg, state, *)
|
16
20
|
reply =
|
17
21
|
if msg == MESSAGE_PREAMBLE + "\r"
|
@@ -23,7 +23,8 @@ module ADAM6050
|
|
23
23
|
|
24
24
|
# @param msg [String] the incomming message.
|
25
25
|
# @param state [Integer] the current state.
|
26
|
-
# @return [
|
26
|
+
# @return [Integer] the next state (always unchanged).
|
27
|
+
# @return [String] the reply.
|
27
28
|
def handle(msg, state, *)
|
28
29
|
channel, value = parse msg
|
29
30
|
next_state = if msg[3] == '1'
|
data/lib/adam6050/password.rb
CHANGED
@@ -15,6 +15,7 @@ module ADAM6050
|
|
15
15
|
# password = Password.new
|
16
16
|
#
|
17
17
|
# password == 'anything' # => true
|
18
|
+
#
|
18
19
|
class Password
|
19
20
|
# Format errors should be raised whenever a plain text password longer than
|
20
21
|
# 8 characters is passed. Note that only ascii characters are supported.
|
@@ -41,6 +42,15 @@ module ADAM6050
|
|
41
42
|
|
42
43
|
private
|
43
44
|
|
45
|
+
# Transforms a plain text password into an 8 character string recognised by
|
46
|
+
# the ADAM-6050. The algorithm, if you can even call it that, used to
|
47
|
+
# perform the transformation was found by trial and error.
|
48
|
+
#
|
49
|
+
# @raise [FormatError] if the plain text password is longer than 8
|
50
|
+
# characters.
|
51
|
+
#
|
52
|
+
# @param plain [String] the plain text version of the password.
|
53
|
+
# @return [String] the obfuscated, 8 character password.
|
44
54
|
def obfuscate(plain)
|
45
55
|
codepoints = plain.codepoints
|
46
56
|
|
data/lib/adam6050/server.rb
CHANGED
@@ -15,6 +15,7 @@ module ADAM6050
|
|
15
15
|
|
16
16
|
# @param password [String] the plain text password to use when validating
|
17
17
|
# new clients.
|
18
|
+
# @param logger [Logger] the logger to use.
|
18
19
|
def initialize(password: nil, logger: Logger.new(STDOUT))
|
19
20
|
@session = Session.new
|
20
21
|
@handlers = [
|
@@ -28,8 +29,17 @@ module ADAM6050
|
|
28
29
|
@logger = logger
|
29
30
|
end
|
30
31
|
|
32
|
+
# Starts a new UDP server that listens on the given port. The state is
|
33
|
+
# updated atomically and yielded to an optional block everytime a change is
|
34
|
+
# made. By returning `false` the block can cancel the state update. This
|
35
|
+
# call is blocking.
|
36
|
+
#
|
37
|
+
# @yield [Integer] the updated state.
|
38
|
+
# @yield [Integer] the old state.
|
39
|
+
#
|
31
40
|
# @param host [String] the host to listen on.
|
32
41
|
# @param port [Integer] the UDP port to listen on.
|
42
|
+
# @return [nil]
|
33
43
|
def run(host: nil, port: DEFAULT_PORT, &block)
|
34
44
|
logger.info "Listening on port #{port}"
|
35
45
|
|
@@ -40,9 +50,13 @@ module ADAM6050
|
|
40
50
|
handle(handler, msg, sender, &block)
|
41
51
|
end
|
42
52
|
end
|
53
|
+
nil
|
43
54
|
end
|
44
55
|
|
45
|
-
# Updates the state atomicly.
|
56
|
+
# Updates the state atomicly. The current state will be yielded to the given
|
57
|
+
# block and the return value used as the next state.
|
58
|
+
#
|
59
|
+
# @yield [Integer] the current state.
|
46
60
|
def update
|
47
61
|
@state_lock.synchronize do
|
48
62
|
@state = yield @state
|
@@ -51,6 +65,13 @@ module ADAM6050
|
|
51
65
|
|
52
66
|
private
|
53
67
|
|
68
|
+
# @yield see #abort_state_change?
|
69
|
+
#
|
70
|
+
# @param handler [Handler] the handler selected to handle the message.
|
71
|
+
# @param msg [String] the received message.
|
72
|
+
# @param sender [UDPSource] the UDP client.
|
73
|
+
# @param block [Proc]
|
74
|
+
# @return [nil]
|
54
75
|
def handle(handler, msg, sender, &block)
|
55
76
|
@session.validate! sender if handler.validate?
|
56
77
|
|
@@ -59,12 +80,22 @@ module ADAM6050
|
|
59
80
|
return if abort_state_change?(next_state, &block)
|
60
81
|
@state = next_state
|
61
82
|
|
62
|
-
|
83
|
+
return unless reply
|
84
|
+
|
85
|
+
sender.reply reply + "\r"
|
86
|
+
logger.debug reply
|
63
87
|
rescue Session::InvalidSender => e
|
64
88
|
sender.reply "?\r"
|
65
89
|
logger.warn e.message
|
66
90
|
end
|
67
91
|
|
92
|
+
# @yield [Integer] the next state.
|
93
|
+
# @yield [Integer] the current state.
|
94
|
+
#
|
95
|
+
# @param next_state [Integer] the next state.
|
96
|
+
# @return [true] if the next state differ from the current and the
|
97
|
+
# (optional) given block returns `false`.
|
98
|
+
# @return [false] otherwise.
|
68
99
|
def abort_state_change?(next_state)
|
69
100
|
return false if next_state == @state
|
70
101
|
|
data/lib/adam6050/session.rb
CHANGED
@@ -32,8 +32,9 @@ module ADAM6050
|
|
32
32
|
# authenticated within the session. This can either be beacuse the sender
|
33
33
|
# has not yet logged in, or beacuse an old login has expired.
|
34
34
|
class InvalidSender < Error
|
35
|
+
# @param sender [Socket::UDPSource] the originating sender.
|
35
36
|
def initialize(sender)
|
36
|
-
super "
|
37
|
+
super "#{self.class.name}: #{sender.remote_address.inspect}"
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
@@ -111,27 +112,43 @@ module ADAM6050
|
|
111
112
|
def cleanup!(time: monotonic_timestamp)
|
112
113
|
return if time < @next_cleanup
|
113
114
|
|
114
|
-
|
115
|
+
delete_expired! time, @timeout
|
115
116
|
|
116
117
|
@next_cleanup = time + @cleanup_interval
|
117
118
|
end
|
118
119
|
|
119
120
|
private
|
120
121
|
|
122
|
+
# @param sender [Socket::UDPSource] the UDP client.
|
123
|
+
# @return [#hash] a unique identifier for the sender.
|
121
124
|
def session_key(sender)
|
122
125
|
sender.remote_address.ip_address
|
123
126
|
end
|
124
127
|
|
128
|
+
# This is slightly faster than calling Time.now since no new object needs to
|
129
|
+
# be allocated.
|
130
|
+
#
|
131
|
+
# @return [Numeric] the current monotonic process time.
|
125
132
|
def monotonic_timestamp
|
126
133
|
Process.clock_gettime Process::CLOCK_MONOTONIC
|
127
134
|
end
|
128
135
|
|
129
|
-
|
136
|
+
# @param last_seen [Numeric] the time when the client was last seen.
|
137
|
+
# @param time [Numeric] the current time.
|
138
|
+
# @param timeout [Numeric] the time after which expired clients should be
|
139
|
+
# deleted.
|
140
|
+
# @return [false] if the time last seen is within the timeout.
|
141
|
+
# @return [true] otherwise.
|
142
|
+
def expired?(last_seen, time, timeout)
|
130
143
|
threshold = time - timeout
|
131
|
-
|
144
|
+
last_seen < threshold
|
132
145
|
end
|
133
146
|
|
134
|
-
|
147
|
+
# @param time [Numeric] the current time.
|
148
|
+
# @param timeout [Numeric] the time after which expired clients should be
|
149
|
+
# deleted.
|
150
|
+
# @return [Hash] the session hash with expired clients deleted.
|
151
|
+
def delete_expired!(time, timeout)
|
135
152
|
@session.delete_if { |_, t| expired? t, time, timeout }
|
136
153
|
end
|
137
154
|
end
|
data/lib/adam6050/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adam6050
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Lindberg
|
@@ -108,8 +108,12 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.9'
|
111
|
-
description: This library implements a server that emulates the
|
112
|
-
IO module.
|
111
|
+
description: This library implements a server that emulates the functionality of the
|
112
|
+
network connected Advantech ADAM-6050 digital IO module. Specifically the UDP protocol
|
113
|
+
that the unit speaks has been reverse engineered. Since I don't have an actual device
|
114
|
+
to test with the response messages from the server may differ from what they should
|
115
|
+
be. It all works well enough for interfacing with Synology Surveillance Station
|
116
|
+
which is the original intent.
|
113
117
|
email:
|
114
118
|
- seb.lindberg@gmail.com
|
115
119
|
executables: []
|