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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 693c25cc61968c917bd119cc16986779e955a29a
4
- data.tar.gz: 72381c1a3a55fd9852bc6cfa4e9b1dec9ba05d6b
3
+ metadata.gz: 2c831e1aa753967132fff58891039d680b8affbc
4
+ data.tar.gz: 27d7aad6b04f7085c8b059e744434fea7939ddb0
5
5
  SHA512:
6
- metadata.gz: 27d756b741f3dd98161fc75c135b906c6d30c451ece1d6c93238ac049a8ef7de24fb6fac0f67d9e23c03f6aa87060bd887a6698ccb6bd66b2233577f8b48a0b6
7
- data.tar.gz: bcafa5b71fa111dd1925ce1a5cfac4ae211709ce09e8e0f33ec3ebf9f72535d8dac5a25050650c5366f15b12ec95b992ea4173d85b23e3731bd98269115fe648
6
+ metadata.gz: aeba0631241b8976c771d32c62e1c9d9ed2d299e6d57b335c01529241e16daf449ee1004294f399d9ad3b2617ba4300baeea29e861c3ae4cff8ebeb6e4758013
7
+ data.tar.gz: 28e971677d1e7befbdec55f875e86f288c62266218a0694506bb17f72b133fe85379ab5eb989ff2512d489df2f72f9365c2f8f932d48164754876f010494b765
@@ -42,6 +42,7 @@ Metrics/BlockLength:
42
42
  - 'Rakefile'
43
43
  - '**/*.rake'
44
44
  - 'test/**/*.rb'
45
+ - '*.gemspec'
45
46
 
46
47
  Metrics/ModuleLength:
47
48
  Exclude:
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- adam6050 (0.1.3)
4
+ adam6050 (0.1.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
- # 🎛 ADAM6050
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
- This library implements a server that emulates the functionality of the network connected Advantech ADAM-6050 IO module.
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.
@@ -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
- 'Advantech ADAM-6050 IO module.'
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 [Array<Integer, String>] the next state and an optional reply.
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 [Array<Integer, String>] the next state and an optional reply.
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 [Array<Integer, String>] the next state and an optional reply.
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 [Array<Integer, String>] the next state and an optional reply.
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'
@@ -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
 
@@ -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
- sender.reply reply + "\r" if reply
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
 
@@ -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 "Invalid sender: #{sender}"
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
- remove_expired! time, @timeout
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
- def expired?(sess_time, time, timeout)
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
- sess_time < threshold
144
+ last_seen < threshold
132
145
  end
133
146
 
134
- def remove_expired!(time, timeout)
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ADAM6050
4
4
  # @return [String] the ADAM6050 library version number.
5
- VERSION = '0.1.3'
5
+ VERSION = '0.1.4'
6
6
  end
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.3
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 Advantech ADAM-6050
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: []