adam6050 0.1.0
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 +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +64 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +24 -0
- data/adam6050.gemspec +37 -0
- data/bin/console +15 -0
- data/bin/server +15 -0
- data/bin/setup +8 -0
- data/lib/adam6050/error.rb +7 -0
- data/lib/adam6050/handler/login.rb +38 -0
- data/lib/adam6050/handler/read.rb +24 -0
- data/lib/adam6050/handler/status.rb +27 -0
- data/lib/adam6050/handler/write.rb +48 -0
- data/lib/adam6050/handler.rb +24 -0
- data/lib/adam6050/password.rb +56 -0
- data/lib/adam6050/server.rb +72 -0
- data/lib/adam6050/session.rb +138 -0
- data/lib/adam6050/state.rb +89 -0
- data/lib/adam6050/version.rb +6 -0
- data/lib/adam6050.rb +22 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 69f18923622d9707c07acccd6433cb10863a3d29
|
4
|
+
data.tar.gz: 29afcfd92332596de33ce33ebdf56b86b44ded07
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 13850fe87f428fe8e5e9b17460a15cb20b7e5979bfce5a581e0cd0311ba87e0d48d10b2d1b04719da16c6186d50d6888d6c1a3440d590c22bc86efdfe8ddda57
|
7
|
+
data.tar.gz: 5363aeb733c37736a815ca087da62c63530de69757831d4996877a8e1fab22f9b853aaf13daed31bba18a811fdb0afc28086897bdf79362efbe2002ca3792311
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- 'vendor/**/*'
|
4
|
+
- 'tmp/**/*'
|
5
|
+
TargetRubyVersion: 2.4
|
6
|
+
|
7
|
+
Style/FrozenStringLiteralComment:
|
8
|
+
EnforcedStyle: always
|
9
|
+
|
10
|
+
Layout/EndOfLine:
|
11
|
+
EnforcedStyle: lf
|
12
|
+
|
13
|
+
Layout/ClassStructure:
|
14
|
+
Enabled: true
|
15
|
+
Categories:
|
16
|
+
module_inclusion:
|
17
|
+
- include
|
18
|
+
- prepend
|
19
|
+
- extend
|
20
|
+
ExpectedOrder:
|
21
|
+
- module_inclusion
|
22
|
+
- constants
|
23
|
+
- public_class_methods
|
24
|
+
- initializer
|
25
|
+
- instance_methods
|
26
|
+
- protected_methods
|
27
|
+
- private_methods
|
28
|
+
|
29
|
+
Layout/IndentHeredoc:
|
30
|
+
EnforcedStyle: squiggly
|
31
|
+
|
32
|
+
Lint/AmbiguousBlockAssociation:
|
33
|
+
Exclude:
|
34
|
+
- 'test/**/*.rb'
|
35
|
+
|
36
|
+
Lint/InterpolationCheck:
|
37
|
+
Exclude:
|
38
|
+
- 'test/**/*.rb'
|
39
|
+
|
40
|
+
Metrics/BlockLength:
|
41
|
+
Exclude:
|
42
|
+
- 'Rakefile'
|
43
|
+
- '**/*.rake'
|
44
|
+
- 'test/**/*.rb'
|
45
|
+
|
46
|
+
Metrics/ModuleLength:
|
47
|
+
Exclude:
|
48
|
+
- 'test/**/*.rb'
|
49
|
+
|
50
|
+
Metrics/ParameterLists:
|
51
|
+
CountKeywordArgs: false
|
52
|
+
|
53
|
+
Naming/UncommunicativeMethodParamName:
|
54
|
+
AllowedNames:
|
55
|
+
- x
|
56
|
+
- y
|
57
|
+
- i
|
58
|
+
- p
|
59
|
+
- n
|
60
|
+
- r
|
61
|
+
- g
|
62
|
+
- b
|
63
|
+
- to
|
64
|
+
- '_'
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
adam6050 (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.0)
|
10
|
+
docile (1.3.1)
|
11
|
+
jaro_winkler (1.5.1)
|
12
|
+
json (2.1.0)
|
13
|
+
minitest (5.11.3)
|
14
|
+
parallel (1.12.1)
|
15
|
+
parser (2.5.1.2)
|
16
|
+
ast (~> 2.4.0)
|
17
|
+
powerpack (0.1.2)
|
18
|
+
rainbow (3.0.0)
|
19
|
+
rake (10.5.0)
|
20
|
+
redcarpet (3.4.0)
|
21
|
+
rubocop (0.58.1)
|
22
|
+
jaro_winkler (~> 1.5.1)
|
23
|
+
parallel (~> 1.10)
|
24
|
+
parser (>= 2.5, != 2.5.1.1)
|
25
|
+
powerpack (~> 0.1)
|
26
|
+
rainbow (>= 2.2.2, < 4.0)
|
27
|
+
ruby-progressbar (~> 1.7)
|
28
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
29
|
+
ruby-progressbar (1.9.0)
|
30
|
+
simplecov (0.16.1)
|
31
|
+
docile (~> 1.1)
|
32
|
+
json (>= 1.8, < 3)
|
33
|
+
simplecov-html (~> 0.10.0)
|
34
|
+
simplecov-html (0.10.2)
|
35
|
+
unicode-display_width (1.4.0)
|
36
|
+
yard (0.9.14)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
adam6050!
|
43
|
+
bundler (~> 1.16)
|
44
|
+
minitest (~> 5.0)
|
45
|
+
rake (~> 10.0)
|
46
|
+
redcarpet (~> 3.4)
|
47
|
+
rubocop (~> 0.52)
|
48
|
+
simplecov (~> 0.16)
|
49
|
+
yard (~> 0.9)
|
50
|
+
|
51
|
+
BUNDLED WITH
|
52
|
+
1.16.3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Sebastian Lindberg
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# 🎛 ADAM6050
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/vissen-input)
|
4
|
+
[](https://travis-ci.org/seblindberg/ruby-adam6050)
|
5
|
+
[](http://inch-ci.org/github/seblindberg/ruby-adam6050)
|
6
|
+
[](http://www.rubydoc.info/gems/adam6050/)
|
7
|
+
|
8
|
+
This library implements a server that emulates the functionality of the network connected Advantech ADAM-6050 IO module.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'adam6050'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install adam6050
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'adam6050'
|
30
|
+
|
31
|
+
server = ADAM6050::Server.new
|
32
|
+
server.run do |state, prev_state|
|
33
|
+
# React to the new state
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Development
|
38
|
+
|
39
|
+
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.
|
40
|
+
|
41
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
42
|
+
|
43
|
+
## Contributing
|
44
|
+
|
45
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/seblindberg/ruby-adam6050.
|
46
|
+
|
47
|
+
## License
|
48
|
+
|
49
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
require 'yard'
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'test'
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.test_files = FileList['test/**/*_test.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new(:rubocop)
|
15
|
+
|
16
|
+
YARD::Rake::YardocTask.new(:yard) do |t|
|
17
|
+
t.stats_options = %w[--list-undoc]
|
18
|
+
t.files = ['lib/**/*.rb', '-', 'CHANGELOG.md']
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Generate Ruby documentation'
|
22
|
+
task doc: %w[yard]
|
23
|
+
|
24
|
+
task default: %w[test rubocop:auto_correct]
|
data/adam6050.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'adam6050/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'adam6050'
|
9
|
+
spec.version = ADAM6050::VERSION
|
10
|
+
spec.authors = ['Sebastian Lindberg']
|
11
|
+
spec.email = ['seb.lindberg@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Server implementation of the ADAM-6050 IO module.'
|
14
|
+
spec.description = 'This library implements a server that emulates the ' \
|
15
|
+
'Advantech ADAM-6050 IO module.'
|
16
|
+
spec.homepage = 'https://github.com/seblindberg/ruby-adam6050'
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added
|
21
|
+
# into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0")
|
24
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = 'exe'
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
31
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'redcarpet', '~> 3.4'
|
34
|
+
spec.add_development_dependency 'rubocop', '~> 0.52'
|
35
|
+
spec.add_development_dependency 'simplecov', '~> 0.16'
|
36
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
37
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'adam6050'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/server
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'adam6050'
|
6
|
+
|
7
|
+
server = ADAM6050::Server.new
|
8
|
+
|
9
|
+
begin
|
10
|
+
server.run do |state|
|
11
|
+
puts ADAM6050::State.inspect(state)
|
12
|
+
end
|
13
|
+
rescue Interrupt
|
14
|
+
puts 'Exiting...'
|
15
|
+
end
|
data/bin/setup
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
module Handler
|
5
|
+
# Allows senders to login.
|
6
|
+
class Login
|
7
|
+
include Handler
|
8
|
+
|
9
|
+
# @return [String] see Handler::MESSAGE_PREAMBLE.
|
10
|
+
MESSAGE_PREAMBLE = '$01PW'
|
11
|
+
|
12
|
+
# @param password [String] the plain text password to use when validating
|
13
|
+
# login requests. If no password is given every request will be granted.
|
14
|
+
def initialize(password = nil)
|
15
|
+
@password = Password.new password
|
16
|
+
freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param msg [String] the incomming message.
|
20
|
+
# @param state [Integer] the current state.
|
21
|
+
#
|
22
|
+
# @return [Array<Integer, String>] the next state and an optional reply.
|
23
|
+
def handle(msg, state, session, sender)
|
24
|
+
return state, nil unless @password == msg[6..-1]
|
25
|
+
|
26
|
+
session.register sender
|
27
|
+
|
28
|
+
[state, '>01']
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [false] the login handler does not require the sender to be
|
32
|
+
# validated.
|
33
|
+
def validate?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
module Handler
|
5
|
+
# Allows registed senders to read the state.
|
6
|
+
class Read
|
7
|
+
include Handler
|
8
|
+
|
9
|
+
# @return [String] see Handler::MESSAGE_PREAMBLE.
|
10
|
+
MESSAGE_PREAMBLE = '$016'
|
11
|
+
|
12
|
+
# @param state [Integer] the current state.
|
13
|
+
# @return [Array<Integer, String>] the next state and an optional reply.
|
14
|
+
def handle(_, state, *)
|
15
|
+
# From the manual:
|
16
|
+
# The first 2-character portion of the response (exclude the "!"
|
17
|
+
# character) indicates the address of the ADAM-6000 module. The second
|
18
|
+
# 2-character portion of the response is reserved, and will always be
|
19
|
+
# 00 currently.
|
20
|
+
[state, '!0100' + State.to_bin(state)]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
module Handler
|
5
|
+
# Allows registed senders to read the IO status.
|
6
|
+
class Status
|
7
|
+
include Handler
|
8
|
+
|
9
|
+
# @return [String] see Handler::MESSAGE_PREAMBLE.
|
10
|
+
MESSAGE_PREAMBLE = '$01C'
|
11
|
+
|
12
|
+
# @param msg [String] the incomming message.
|
13
|
+
# @param state [Integer] the current state.
|
14
|
+
# @return [Array<Integer, String>] the next state and an optional reply.
|
15
|
+
def handle(msg, state, *)
|
16
|
+
reply =
|
17
|
+
if msg == MESSAGE_PREAMBLE + "\r"
|
18
|
+
'!01' + '000000000000' + '000000000000' + '000000000000'
|
19
|
+
else
|
20
|
+
'>'
|
21
|
+
end
|
22
|
+
|
23
|
+
[state, reply]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
module Handler
|
5
|
+
# Allows registed senders to change the output bits of the state.
|
6
|
+
#
|
7
|
+
# From the manual:
|
8
|
+
# Name Write Digital Output
|
9
|
+
# Description This command sets a single or all digital output channels to
|
10
|
+
# the specific ADAM-6000 module.
|
11
|
+
# Syntax #01bb(data)\r
|
12
|
+
# bb is used to indicate which channel(s) you want to set.
|
13
|
+
# Writing to all channels (write a byte): both characters
|
14
|
+
# should be equal to zero (BB=00).
|
15
|
+
# Writing to a single channel (write a bit): first character
|
16
|
+
# is 1, second character indicates channel number which can
|
17
|
+
# range from 0h to Fh.
|
18
|
+
class Write
|
19
|
+
include Handler
|
20
|
+
|
21
|
+
# @return [String] see Handler::MESSAGE_PREAMBLE.
|
22
|
+
MESSAGE_PREAMBLE = '#01'
|
23
|
+
|
24
|
+
# @param msg [String] the incomming message.
|
25
|
+
# @param state [Integer] the current state.
|
26
|
+
# @return [Array<Integer, String>] the next state and an optional reply.
|
27
|
+
def handle(msg, state, *)
|
28
|
+
channel, value = parse msg
|
29
|
+
next_state = if msg[3] == '1'
|
30
|
+
State.update state, channel, value
|
31
|
+
else
|
32
|
+
State.update_all state, value
|
33
|
+
end
|
34
|
+
|
35
|
+
[next_state, '>']
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def parse(msg)
|
41
|
+
[
|
42
|
+
msg[4].to_i(16),
|
43
|
+
msg[5..6].to_i(16)
|
44
|
+
]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
# Handlers are, for the most part, simple transformations that accept a
|
5
|
+
# _state_, an incomming _message_, a _session_ and a _sender_ and produce a
|
6
|
+
# new state as well as an optional reply. The only
|
7
|
+
module Handler
|
8
|
+
# @return [String] the first letters of the message that should be used when
|
9
|
+
# determining if the handler can handle it.
|
10
|
+
MESSAGE_PREAMBLE = '$01'
|
11
|
+
|
12
|
+
# @param msg [String] the incomming message.
|
13
|
+
# @return [true] if the handler can handle the message.
|
14
|
+
# @return [false] otherwise.
|
15
|
+
def handles?(msg)
|
16
|
+
msg.start_with? self.class::MESSAGE_PREAMBLE
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [true] if the handler requires the sender to be validated.
|
20
|
+
def validate?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
# == Usage
|
5
|
+
# The following example creates a password and uses it to validate an encoded
|
6
|
+
# string.
|
7
|
+
#
|
8
|
+
# password = Password.new 'b6TSkfr6'
|
9
|
+
#
|
10
|
+
# password == 'b6TSkfr6' # => false
|
11
|
+
# password == "]\tklTYM\t" # => true
|
12
|
+
#
|
13
|
+
# The next example creates a password that will match any string.
|
14
|
+
#
|
15
|
+
# password = Password.new
|
16
|
+
#
|
17
|
+
# password == 'anything' # => true
|
18
|
+
class Password
|
19
|
+
# Format errors should be raised whenever a plain text password longer than
|
20
|
+
# 8 characters is passed. Note that only ascii characters are supported.
|
21
|
+
class FormatError < Error
|
22
|
+
def initialize
|
23
|
+
super 'Only ascii passwords of length 8 or less are supported'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @raise [FormatError] if the plain text password is longer than 8
|
28
|
+
# characters.
|
29
|
+
#
|
30
|
+
# @param plain [String] the plain text version of the password.
|
31
|
+
def initialize(plain = nil)
|
32
|
+
if plain
|
33
|
+
password = obfuscate plain
|
34
|
+
define_singleton_method(:==) { |text| password == text }
|
35
|
+
else
|
36
|
+
define_singleton_method(:==) { |_| true }
|
37
|
+
end
|
38
|
+
|
39
|
+
freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def obfuscate(plain)
|
45
|
+
codepoints = plain.codepoints
|
46
|
+
|
47
|
+
raise FormatError if codepoints.length > 8
|
48
|
+
|
49
|
+
password = Array.new(8, 0x0E)
|
50
|
+
codepoints.each_with_index do |c, i|
|
51
|
+
password[i] = (c & 0x40) | (~c & 0x3F)
|
52
|
+
end
|
53
|
+
password.pack 'c*'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
# The server listens to a speciefied UDP port and delegates incomming messages
|
5
|
+
# to the different handlers.
|
6
|
+
class Server
|
7
|
+
# @return [Integer] the dafault port of the UDP server.
|
8
|
+
DEFAULT_PORT = 1025
|
9
|
+
|
10
|
+
# @return [Logger] the logger used by the server.
|
11
|
+
attr_reader :logger
|
12
|
+
|
13
|
+
# @param password [String] the plain text password to use when validating
|
14
|
+
# new clients.
|
15
|
+
def initialize(password: nil, logger: Logger.new(STDOUT))
|
16
|
+
@session = Session.new
|
17
|
+
@handlers = [
|
18
|
+
Handler::Login.new(password),
|
19
|
+
Handler::Status.new,
|
20
|
+
Handler::Read.new,
|
21
|
+
Handler::Write.new
|
22
|
+
]
|
23
|
+
@state = State.initial
|
24
|
+
@state_lock = Mutex.new
|
25
|
+
@logger = logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param host [String] the host to listen on.
|
29
|
+
# @param port [Integer] the UDP port to listen on.
|
30
|
+
def run(host: nil, port: DEFAULT_PORT, &block)
|
31
|
+
logger.info "Listening on port #{port}"
|
32
|
+
|
33
|
+
Socket.udp_server_loop host, port do |msg, sender|
|
34
|
+
logger.debug { "#{sender.remote_address} -> '#{msg}'" }
|
35
|
+
handler = @handlers.find { |h| h.handles? msg } || next
|
36
|
+
|
37
|
+
@state_lock.synchronize do
|
38
|
+
handle(handler, msg, sender, &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Updates the state atomicly.
|
44
|
+
def update
|
45
|
+
@state_lock.synchronize do
|
46
|
+
@state = yield @state
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def handle(handler, msg, sender, &block)
|
53
|
+
@session.validate! sender if handler.validate?
|
54
|
+
|
55
|
+
next_state, reply = handler.handle msg, @state, @session, sender
|
56
|
+
|
57
|
+
return if abort_state_change?(next_state, &block)
|
58
|
+
|
59
|
+
sender.reply reply + "\r" if reply
|
60
|
+
@state = next_state
|
61
|
+
rescue Session::InvalidSender => e
|
62
|
+
logger.warn e.message
|
63
|
+
end
|
64
|
+
|
65
|
+
def abort_state_change?(next_state)
|
66
|
+
return true if next_state == @state
|
67
|
+
|
68
|
+
commit = !block_given? || yield(next_state, @state)
|
69
|
+
commit == false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
# The session object is used by the server to keep track of authenticated
|
5
|
+
# clients. Once a client is registered with the session it will be reported as
|
6
|
+
# valid for a period of time, after which it is again marked as invalid and
|
7
|
+
# needs to register again. Any activity before the timeout will reset the
|
8
|
+
# countdown.
|
9
|
+
#
|
10
|
+
# == Usage
|
11
|
+
# The following example creates a session with a 10 second timeout and
|
12
|
+
# registers a sender. `#validate!` is then called at a later point in time to
|
13
|
+
# verify that the sender is still valid.
|
14
|
+
#
|
15
|
+
# session = Session.new timeout: 10.0
|
16
|
+
# session.register sender
|
17
|
+
#
|
18
|
+
# # The following call will raise an exception if more
|
19
|
+
# # than 10 seconds has passed.
|
20
|
+
# session.validate! sender
|
21
|
+
#
|
22
|
+
class Session
|
23
|
+
# @return [Numeric] the default number of seconds a sender is valid with no
|
24
|
+
# interaction.
|
25
|
+
DEFAULT_TIMEOUT = 60.0
|
26
|
+
|
27
|
+
# @return [Numeric] the default number of seconds to wait after one cleanup
|
28
|
+
# before perfoming the next.
|
29
|
+
DEFAULT_CLEANUP_INTERVALL = 3600.0
|
30
|
+
|
31
|
+
# The invalid sender error is used to signify that a sender is not
|
32
|
+
# authenticated within the session. This can either be beacuse the sender
|
33
|
+
# has not yet logged in, or beacuse an old login has expired.
|
34
|
+
class InvalidSender < Error
|
35
|
+
def initialize(sender)
|
36
|
+
super "Invalid sender: #{sender}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# The unknown sender error is used to signify that a sender has not yet
|
41
|
+
# authenticated within the session.
|
42
|
+
class UnknownSender < InvalidSender; end
|
43
|
+
|
44
|
+
# @param timeout [Numeric] the number of seconds a sender is valid with no
|
45
|
+
# interaction.
|
46
|
+
# @param cleanup_interval [Numeric] the number of seconds to wait after one
|
47
|
+
# cleanup before perfoming the next.
|
48
|
+
def initialize(timeout: DEFAULT_TIMEOUT,
|
49
|
+
cleanup_interval: DEFAULT_CLEANUP_INTERVALL)
|
50
|
+
@session = {}
|
51
|
+
@timeout = timeout
|
52
|
+
@cleanup_interval = cleanup_interval
|
53
|
+
@next_cleanup = 0.0
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Integer] the number of senders currently known by the session.
|
57
|
+
# Note that this may include invalid senders that have not yet been
|
58
|
+
# cleaned up.
|
59
|
+
def size
|
60
|
+
@session.size
|
61
|
+
end
|
62
|
+
|
63
|
+
# Register a new sender as valid in the current session.
|
64
|
+
#
|
65
|
+
# @param sender [Socket::UDPSource] the udp client.
|
66
|
+
# @param time [Numeric] the current time. The current time will be used if
|
67
|
+
# not specified.
|
68
|
+
# @return [nil]
|
69
|
+
def register(sender, time: monotonic_timestamp)
|
70
|
+
@session[session_key sender] = time
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# A sender is valid as long as it is registered and has not expired within
|
75
|
+
# the current session.
|
76
|
+
#
|
77
|
+
# @param sender [Socket::UDPSource] the udp client.
|
78
|
+
# @param time [Numeric] the current time. The current time will be used if
|
79
|
+
# not specified.
|
80
|
+
# @return [true] if the sender has authenticated and has been active within
|
81
|
+
# the configured timeout.
|
82
|
+
# @return [false] otherwise.
|
83
|
+
def valid?(sender, time: monotonic_timestamp)
|
84
|
+
!expired? @session.fetch(session_key(sender), 0.0), time, @timeout
|
85
|
+
end
|
86
|
+
|
87
|
+
# Renews the given sender if it is still valid within the session and raises
|
88
|
+
# an exception otherwise.
|
89
|
+
#
|
90
|
+
# @raise [UnknownSender] if the given sender is not registered.
|
91
|
+
# @raise [InvalidSender] if the given sender is not valid.
|
92
|
+
#
|
93
|
+
# @param sender [Socket::UDPSource] the udp client.
|
94
|
+
# @param time [Numeric] the current time. The current time will be used if
|
95
|
+
# not specified.
|
96
|
+
# @return [nil]
|
97
|
+
def validate!(sender, time: monotonic_timestamp)
|
98
|
+
key = session_key sender
|
99
|
+
last_observed = @session.fetch(key) { raise UnknownSender, sender }
|
100
|
+
raise InvalidSender, sender if expired? last_observed, time, @timeout
|
101
|
+
|
102
|
+
@session[key] = time
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
# Removes invalid senders from the session.
|
107
|
+
#
|
108
|
+
# @param time [Numeric] the current time. The current time will be used if
|
109
|
+
# not specified.
|
110
|
+
# @return [Numeric] the next time before which no cleanup will be performed.
|
111
|
+
def cleanup!(time: monotonic_timestamp)
|
112
|
+
return if time < @next_cleanup
|
113
|
+
|
114
|
+
remove_expired! time, @timeout
|
115
|
+
|
116
|
+
@next_cleanup = time + @cleanup_interval
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def session_key(sender)
|
122
|
+
sender.remote_address.to_sockaddr
|
123
|
+
end
|
124
|
+
|
125
|
+
def monotonic_timestamp
|
126
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC
|
127
|
+
end
|
128
|
+
|
129
|
+
def expired?(sess_time, time, timeout)
|
130
|
+
threshold = time - timeout
|
131
|
+
sess_time < threshold
|
132
|
+
end
|
133
|
+
|
134
|
+
def remove_expired!(time, timeout)
|
135
|
+
@session.delete_if { |_, t| expired? t, time, timeout }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ADAM6050
|
4
|
+
# The application state is stored as an integer an updated in an immutable
|
5
|
+
# fashion. This module includes helper functions that simplify reading and
|
6
|
+
# creating new states.
|
7
|
+
module State
|
8
|
+
# @return [Integer] the number of inputs.
|
9
|
+
NUM_INPUTS = 12
|
10
|
+
|
11
|
+
# @return [Integer] the number of outputs.
|
12
|
+
NUM_OUTPUTS = 6
|
13
|
+
|
14
|
+
# @return [Integer] a binary mask selecting the bits of an integer used by
|
15
|
+
# the state.
|
16
|
+
MASK = (1 << (NUM_INPUTS + NUM_OUTPUTS)) - 1
|
17
|
+
|
18
|
+
INPUT_MASK = (1 << NUM_INPUTS) - 1
|
19
|
+
OUTPUT_MASK = MASK - INPUT_MASK
|
20
|
+
|
21
|
+
private_constant :MASK, :INPUT_MASK, :OUTPUT_MASK
|
22
|
+
|
23
|
+
# @return [Integer] the initial state.
|
24
|
+
def initial
|
25
|
+
0
|
26
|
+
end
|
27
|
+
|
28
|
+
# @raise [RangeError] if the given channel index exceeds the number of
|
29
|
+
# available input channels.
|
30
|
+
#
|
31
|
+
# @param state [Integer] the current state.
|
32
|
+
# @param input_channel [Integer] the input channel number.
|
33
|
+
# @return [true, false] the state of the specified input.
|
34
|
+
def input_set?(state, input_channel)
|
35
|
+
raise RangeError if input_channel >= NUM_INPUTS
|
36
|
+
|
37
|
+
state & (1 << input_channel) != 0
|
38
|
+
end
|
39
|
+
|
40
|
+
# @raise [RangeError] if the given channel index exceeds the number of
|
41
|
+
# available output channels.
|
42
|
+
#
|
43
|
+
# @param state [Integer] the current state.
|
44
|
+
# @param output_channel [Integer] the output channel number.
|
45
|
+
# @return [true, false] the state of the specified output.
|
46
|
+
def output_set?(state, output_channel)
|
47
|
+
raise RangeError if output_channel >= NUM_OUTPUTS
|
48
|
+
|
49
|
+
state & (1 << output_channel + NUM_INPUTS) != 0
|
50
|
+
end
|
51
|
+
|
52
|
+
# @raise [RangeError] if the given channel index exceeds the number of
|
53
|
+
# available output channels.
|
54
|
+
#
|
55
|
+
# @param state [Integer] the current state.
|
56
|
+
# @param output_channel [Integer] the output channel number.
|
57
|
+
# @param value [0,Integer] the value to update with.
|
58
|
+
# @return [Integer] the next state.
|
59
|
+
def update(state, output_channel, value)
|
60
|
+
raise RangeError if output_channel >= NUM_OUTPUTS
|
61
|
+
|
62
|
+
mask = (1 << output_channel + NUM_INPUTS)
|
63
|
+
value.zero? ? state & ~mask : state | mask
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param state [Integer] the current state.
|
67
|
+
# @param values [Integer] the next output values.
|
68
|
+
# @return [Integer] the next state.
|
69
|
+
def update_all(state, values)
|
70
|
+
state & INPUT_MASK | (values << NUM_INPUTS) & MASK
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param state [Integer] the current state.
|
74
|
+
# @return [String] a string representation of the state.
|
75
|
+
def inspect(state)
|
76
|
+
compact = format '%018b', state
|
77
|
+
compact[0...6] + ' ' + compact[6..-1]
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param state [Integer] the current state.
|
81
|
+
# @return [String] a binary representation expected by the protocol.
|
82
|
+
def to_bin(state)
|
83
|
+
format '%05X', (~state & MASK)
|
84
|
+
end
|
85
|
+
|
86
|
+
module_function :initial, :input_set?, :output_set?, :update, :update_all,
|
87
|
+
:inspect, :to_bin
|
88
|
+
end
|
89
|
+
end
|
data/lib/adam6050.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
require 'adam6050/error'
|
7
|
+
require 'adam6050/password'
|
8
|
+
require 'adam6050/session'
|
9
|
+
require 'adam6050/state'
|
10
|
+
|
11
|
+
require 'adam6050/handler'
|
12
|
+
require 'adam6050/handler/login'
|
13
|
+
require 'adam6050/handler/read'
|
14
|
+
require 'adam6050/handler/status'
|
15
|
+
require 'adam6050/handler/write'
|
16
|
+
|
17
|
+
require 'adam6050/server'
|
18
|
+
|
19
|
+
# This library implements a server that emulates the Advantech ADAM-6050 IO
|
20
|
+
# module.
|
21
|
+
module ADAM6050
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adam6050
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Lindberg
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-07-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: redcarpet
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.52'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.52'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.16'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.16'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
111
|
+
description: This library implements a server that emulates the Advantech ADAM-6050
|
112
|
+
IO module.
|
113
|
+
email:
|
114
|
+
- seb.lindberg@gmail.com
|
115
|
+
executables: []
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".gitignore"
|
120
|
+
- ".rubocop.yml"
|
121
|
+
- ".travis.yml"
|
122
|
+
- CHANGELOG.md
|
123
|
+
- Gemfile
|
124
|
+
- Gemfile.lock
|
125
|
+
- LICENSE.txt
|
126
|
+
- README.md
|
127
|
+
- Rakefile
|
128
|
+
- adam6050.gemspec
|
129
|
+
- bin/console
|
130
|
+
- bin/server
|
131
|
+
- bin/setup
|
132
|
+
- lib/adam6050.rb
|
133
|
+
- lib/adam6050/error.rb
|
134
|
+
- lib/adam6050/handler.rb
|
135
|
+
- lib/adam6050/handler/login.rb
|
136
|
+
- lib/adam6050/handler/read.rb
|
137
|
+
- lib/adam6050/handler/status.rb
|
138
|
+
- lib/adam6050/handler/write.rb
|
139
|
+
- lib/adam6050/password.rb
|
140
|
+
- lib/adam6050/server.rb
|
141
|
+
- lib/adam6050/session.rb
|
142
|
+
- lib/adam6050/state.rb
|
143
|
+
- lib/adam6050/version.rb
|
144
|
+
homepage: https://github.com/seblindberg/ruby-adam6050
|
145
|
+
licenses:
|
146
|
+
- MIT
|
147
|
+
metadata: {}
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubyforge_project:
|
164
|
+
rubygems_version: 2.6.11
|
165
|
+
signing_key:
|
166
|
+
specification_version: 4
|
167
|
+
summary: Server implementation of the ADAM-6050 IO module.
|
168
|
+
test_files: []
|