mc_protocol_e 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b36cc1712b0d48b344b81c44b982cb5f5d55dc0bb392416cd39ef74ea625943a
4
+ data.tar.gz: b87f147713b5ecb2170ae12858d59fef773cec849a3d34858eae14b873b20a2b
5
+ SHA512:
6
+ metadata.gz: cf757452f3bf39db9311d5de2a1da4538d599e8b37c5e69da93cd5220b09e8a7e616e8b1906684cec05920c54ee88b7a95858da005106e051380dcc4d3f2b6a9
7
+ data.tar.gz: b0905894fafd044aa1c01068d1f2f82d4d7c9569d15ecda2e4cdedf6929bcb9cad6fd7b8d42a355d0ff5d323054e8ec4028b1c483ab2295d1e80d3b55126d6c3
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ /.idea/
14
+ /vendor/
15
+ .rubocop-*
16
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,3 @@
1
+ ---
2
+ inherit_from:
3
+ - https://gist.githubusercontent.com/commis1059/5ce7db028de5fe00fe88392ff7a95278/raw/c00065917dada0e4aa40669fcde90411e70ce82c/.rubocop.yml
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.1
7
+ before_install: gem install bundler -v 2.0.1
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at yusuke.saito@smartscape.co.jp. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in mc_protocol_e.gemspec
6
+ gemspec
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mc_protocol_e (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.3)
10
+ rake (10.5.0)
11
+ rspec (3.8.0)
12
+ rspec-core (~> 3.8.0)
13
+ rspec-expectations (~> 3.8.0)
14
+ rspec-mocks (~> 3.8.0)
15
+ rspec-core (3.8.0)
16
+ rspec-support (~> 3.8.0)
17
+ rspec-expectations (3.8.2)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.8.0)
20
+ rspec-mocks (3.8.0)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.8.0)
23
+ rspec-support (3.8.0)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ bundler (~> 2.0)
30
+ mc_protocol_e!
31
+ rake (~> 10.0)
32
+ rspec (~> 3.0)
33
+
34
+ BUNDLED WITH
35
+ 2.0.1
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 yusuke.saito
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.
@@ -0,0 +1,105 @@
1
+ # McProtocolE
2
+
3
+ McProtocolE is an implementation of MC protocol client over ethernet by Ruby. MC protocol is communication protocol to access MELSEC (PLC). See below for details.
4
+
5
+ https://dl.mitsubishielectric.com/dl/fa/document/manual/plc/sh080008/sh080008x.pdf
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'mc_protocol_e'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install mc_protocol_e
22
+
23
+ ## Example
24
+ Below is an example of use for this library.
25
+
26
+ ```ruby
27
+ require 'optparse'
28
+ require_relative 'frame_1e/request'
29
+ require_relative 'frame_1e/access_route'
30
+ require_relative 'frame_1e/device_range'
31
+ require_relative 'frame_3e/request'
32
+ require_relative 'frame_3e/access_route'
33
+ require_relative 'frame_3e/device_range'
34
+
35
+ opts = ARGV.getopts("h:p:f:", "rw:", "device_num:", "device_points:", "values:")
36
+ raise ArgumentError, "required option is not specified" unless %w[h p f device_num device_points].all? {|key| opts[key] }
37
+
38
+ req =
39
+ case opts["f"]
40
+ when "1e"
41
+ case opts["rw"]
42
+ when "r"
43
+ McProtocolE::Frame1e::Request.batch_read_in_word(
44
+ access_route: McProtocolE::Frame1e::AccessRoute.own_station,
45
+ wait_sec: 3,
46
+ device_range: McProtocolE::Frame1e::DeviceRange.data_register(device_num: opts["device_num"].to_i, device_points: opts["device_points"].to_i)
47
+ )
48
+ when "w"
49
+ raise ArgumentError, "values is required" unless opts["values"]
50
+
51
+ McProtocolE::Frame1e::Request.batch_write_in_word(
52
+ access_route: McProtocolE::Frame1e::AccessRoute.own_station,
53
+ wait_sec: 3,
54
+ device_range: McProtocolE::Frame1e::DeviceRange.data_register(device_num: opts["device_num"].to_i, device_points: opts["device_points"].to_i),
55
+ values: opts["values"].split(",").map(&:to_i),
56
+ )
57
+ else
58
+ raise ArgumentError
59
+ end
60
+ when "3e"
61
+ case opts["rw"]
62
+ when "r"
63
+ McProtocolE::Frame3e::Request.batch_read_in_word(
64
+ access_route: McProtocolE::Frame3e::AccessRoute.own_station,
65
+ wait_sec: 3,
66
+ device_range: McProtocolE::Frame3e::DeviceRange.data_register(device_num: opts["device_num"].to_i, device_points: opts["device_points"].to_i)
67
+ )
68
+ when "w"
69
+ raise ArgumentError, "values is required" unless opts["values"]
70
+
71
+ McProtocolE::Frame3e::Request.batch_write_in_word(
72
+ access_route: McProtocolE::Frame3e::AccessRoute.own_station,
73
+ wait_sec: 3,
74
+ device_range: McProtocolE::Frame3e::DeviceRange.data_register(device_num: opts["device_num"].to_i, device_points: opts["device_points"].to_i),
75
+ values: opts["values"].split(",").map(&:to_i),
76
+ )
77
+ else
78
+ raise ArgumentError
79
+ end
80
+ else
81
+ raise ArgumentError
82
+ end
83
+
84
+ McProtocolE::Client.start(address: opts["h"], port: opts["p"].to_i) {|client|
85
+ pp req.to_b
86
+ res = client.request(req)
87
+ pp res.map {|raw| raw.unpack("s").first }
88
+ }
89
+
90
+ ```
91
+
92
+ ## Limitations
93
+
94
+ * Support only TCP.
95
+ * Support only binary code.
96
+ * Support only below frames.
97
+ * 3E frame
98
+ * 1E frame
99
+ * Support only below commands.
100
+ * Batch read in word units (0401)
101
+ * Batch write in word units (1401)
102
+
103
+ ## License
104
+
105
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "mc_protocol_e"
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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mc_protocol_e/client"
4
+ require "mc_protocol_e/version"
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ module McProtocolE
6
+ # This client shows a MC protocol client.
7
+ class Client
8
+
9
+ class NotStartedError < StandardError; end
10
+
11
+ DEFAULT_OPEN_TIMEOUT = 3
12
+ DEFAULT_READ_TIMEOUT = 3
13
+
14
+ # Constructor.
15
+ # @param [String] address server IP address
16
+ # @param [Integer] port server port number
17
+ # @param [Numeric] open_timeout second of open timeout
18
+ # @param [Numeric] read_timeout second of read timeout
19
+ def initialize(address:, port:, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
20
+ @address = address
21
+ @port = port
22
+ @open_timeout = open_timeout
23
+ @read_timeout = read_timeout
24
+ @socket = nil
25
+ end
26
+
27
+ # Starts MC protocol communication.
28
+ # @param [String] address IP address
29
+ # @param [Integer] port port number
30
+ # @param [Numeric] open_timeout second of open timeout
31
+ # @param [Numeric] read_timeout second of read timeout
32
+ # @yield [client] MC protocol client
33
+ def self.start(address:, port:, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT, &block)
34
+ client = new(address: address, port: port, open_timeout: open_timeout, read_timeout: read_timeout)
35
+
36
+ if block_given?
37
+ client.start(&block)
38
+ else
39
+ client
40
+ end
41
+ end
42
+
43
+ # Closes MC protocol communication.
44
+ def close
45
+ socket.close if started?
46
+ end
47
+
48
+ # Sends request.
49
+ # @param [Request] req request
50
+ # @return [Object] response
51
+ # @raise [NotStartedError] when not started
52
+ def request(req)
53
+ raise NotStartedError, "not started" unless started?
54
+
55
+ req.exec(socket, read_timeout)
56
+ end
57
+
58
+ # Starts MC protocol communication.
59
+ def start
60
+ @socket = Socket.tcp(address, port, connect_timeout: open_timeout) unless started?
61
+
62
+ if block_given?
63
+ begin
64
+ yield self
65
+ ensure
66
+ close
67
+ end
68
+ else
69
+ self
70
+ end
71
+ end
72
+
73
+ # Returns true if communication has started.
74
+ def started?
75
+ socket && !socket.closed?
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :address, :port, :open_timeout, :read_timeout, :socket
81
+
82
+ end
83
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McProtocolE
4
+ module Frame1e
5
+ # This class shows a access route.
6
+ class AccessRoute
7
+
8
+ OWN_PC_NUM = 255
9
+
10
+ attr_reader :pc_num
11
+
12
+ # Constructor.
13
+ # @param [Integer] pc_num PC number
14
+ def initialize(pc_num:)
15
+ @pc_num = pc_num
16
+ end
17
+
18
+ # Returns instance of own station.
19
+ # @return [AccessRoute] a access route to own station
20
+ def self.own_station
21
+ new(pc_num: OWN_PC_NUM)
22
+ end
23
+
24
+ # Returns binary string.
25
+ # @return [String] binary string
26
+ def to_b
27
+ [pc_num].pack("v")[0]
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module McProtocolE
6
+ module Frame1e
7
+ # This class shows a command to batch read in word.
8
+ class BatchReadInWord
9
+ extend Forwardable
10
+
11
+ REQUEST_HEADER = "\x01".b
12
+ RESPONSE_HEADER = "\x81".b
13
+
14
+ attr_reader :request_header
15
+ def_delegators :@device_range, :to_b
16
+
17
+ # Constructor.
18
+ # @param [DeviceRange] device_range device range
19
+ def initialize(device_range:)
20
+ @request_header = REQUEST_HEADER
21
+ @device_range = device_range
22
+ end
23
+
24
+ # Returns array of word.
25
+ # @param [Response] res response
26
+ # @return[Array] array of word
27
+ def parse(res)
28
+ res.data&.each_char&.each_slice(2)&.map(&:join)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :device_range
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module McProtocolE
6
+ module Frame1e
7
+ # This class shows a command to batch read in word.
8
+ class BatchWriteInWord
9
+ extend Forwardable
10
+
11
+ REQUEST_HEADER = "\x03".b
12
+ RESPONSE_HEADER = "\x83".b
13
+
14
+ attr_reader :request_header
15
+
16
+ # Constructor.
17
+ # @param [DeviceRange] device_range device range
18
+ # @param [Array<Integer>] values values to write
19
+ # @raise [ArgumentError] when arguments is invalid
20
+ def initialize(device_range:, values:)
21
+ @request_header = REQUEST_HEADER
22
+ @device_range = device_range
23
+ @values = values
24
+
25
+ validate
26
+ end
27
+
28
+ # Returns array of word.
29
+ # @param [Response] res response
30
+ # @return[Array] array of word
31
+ def parse(res)
32
+ res.data&.each_char&.each_slice(2)&.map(&:join)
33
+ end
34
+
35
+ # Returns binary string.
36
+ # @return [String] binary string
37
+ def to_b
38
+ @to_b ||= device_range.to_b + values.pack("v*")
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :device_range, :values
44
+
45
+ def validate
46
+ raise ArgumentError, "device points have to equal values size" unless device_range.size == values.size
47
+ raise ArgumentError, "all value have to be an integer" unless values.all? {|value| value.is_a?(Integer) }
48
+
49
+ true
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McProtocolE
4
+ module Frame1e
5
+ # This class shows a device range.
6
+ class DeviceRange
7
+
8
+ FIXED_VALUE = "\x00".b
9
+
10
+ module DeviceCode
11
+ DATA_REGISTER = "\x20\x44".b
12
+ end
13
+
14
+ # Constructor.
15
+ # @param [String] device_code device code
16
+ # @param [Integer] device_num first device number in range
17
+ # @param [Integer] device_points number in range
18
+ def initialize(device_code:, device_num:, device_points:)
19
+ @device_code = device_code
20
+ @device_num = device_num
21
+ @device_points = device_points
22
+ end
23
+
24
+ # Returns device range of data register.
25
+ # @param [Integer] device_num first device number in range
26
+ # @param [Integer] device_points number in range
27
+ def self.data_register(device_num:, device_points:)
28
+ new(device_code: DeviceCode::DATA_REGISTER, device_num: device_num, device_points: device_points)
29
+ end
30
+
31
+ # Returns range size.
32
+ # @return [Integer] range size
33
+ def size
34
+ device_points
35
+ end
36
+
37
+ # Returns binary string.
38
+ # @return [String] binary string
39
+ def to_b
40
+ [[device_num].pack("V"), device_code, [device_points].pack("v")[0], FIXED_VALUE].join
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :device_code, :device_num, :device_points
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'batch_read_in_word'
4
+ require_relative 'batch_write_in_word'
5
+ require_relative 'response'
6
+
7
+ module McProtocolE
8
+ module Frame1e
9
+ # This class shows a request of MC protocol.
10
+ class Request
11
+
12
+ DEFAULT_READ_TIMEOUT = 3
13
+
14
+ # Constructor.
15
+ # @param [AccessRoute] access_route access route
16
+ # @param [Numeric] wait_sec waiting second
17
+ # @param [Object] command command of MC protocol
18
+ def initialize(access_route:, wait_sec:, command:)
19
+ @sub_header = command.request_header
20
+ @access_route = access_route
21
+ @wait_sec = WaitSec.new(wait_sec)
22
+ @command = command
23
+ end
24
+
25
+ # Returns a request to batch read.
26
+ # @param [AccessRoute] access_route access route
27
+ # @param [Numeric] wait_sec waiting second
28
+ # @param [DeviceRange] device_range device_range
29
+ def self.batch_read_in_word(access_route:, wait_sec:, device_range:)
30
+ new(access_route: access_route, wait_sec: wait_sec, command: BatchReadInWord.new(device_range: device_range))
31
+ end
32
+
33
+ # Returns a request to batch write.
34
+ # @param [AccessRoute] access_route access route
35
+ # @param [Numeric] wait_sec waiting second
36
+ # @param [DeviceRange] device_range device_range
37
+ # @param [Array] values values to write
38
+ def self.batch_write_in_word(access_route:, wait_sec:, device_range:, values:)
39
+ new(access_route: access_route, wait_sec: wait_sec, command: BatchWriteInWord.new(device_range: device_range, values: values))
40
+ end
41
+
42
+ # Writes and returns a response.
43
+ # @param [IO] socket TCP socket
44
+ # @param [Integer] read_timeout read timeout second
45
+ # @return [Object] responce
46
+ def exec(socket, read_timeout = DEFAULT_READ_TIMEOUT)
47
+ socket.write(to_b)
48
+ res = Response.recv(socket, read_timeout)
49
+ command.parse(res)
50
+ end
51
+
52
+ # Returns binary string.
53
+ # @return [String] binary string
54
+ def to_b
55
+ sub_header + access_route.to_b + wait_sec.to_b + command.to_b
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :sub_header, :access_route, :wait_sec, :command
61
+
62
+ end
63
+
64
+ # This class shows waiting second for MC protocol.
65
+ class WaitSec
66
+
67
+ def initialize(sec)
68
+ @sec = sec
69
+ end
70
+
71
+ def to_b
72
+ return @binary if @binary
73
+
74
+ quarter_sec = (sec * 4).to_i
75
+ @binary = [quarter_sec].pack("v")
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :sec
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+
5
+ module McProtocolE
6
+ module Frame1e
7
+ # This class shows a responce of MC protocol.
8
+ class Response
9
+
10
+ class TimeoutError < Timeout::Error; end
11
+
12
+ DEFAULT_READ_TIMEOUT = 3
13
+ MAX_RECV_LEN = 1024 * 1024
14
+ SUCCEED_CODE = 0
15
+
16
+ attr_reader :data
17
+
18
+ # Constructor.
19
+ # @param [String] raw_res binary string of response
20
+ def initialize(raw_res)
21
+ @sub_header = raw_res[0]
22
+ @code = raw_res[1].unpack1("C")
23
+ @data = raw_res[2..-1] || ""
24
+ end
25
+
26
+ # Receives responce and returns.
27
+ # @param [IO] socket TCP socket
28
+ # @param [Integer] read_timeout read timeout second
29
+ # @return [Responce] received a response
30
+ def self.recv(socket, read_timeout = DEFAULT_READ_TIMEOUT)
31
+ selected = IO.select([socket], nil, nil, read_timeout)
32
+ raise TimeoutError unless selected
33
+
34
+ raw_res = socket.recv(MAX_RECV_LEN)
35
+ new(raw_res)
36
+ end
37
+
38
+ # Returns true if a command succeed.
39
+ def succeed?
40
+ code == SUCCEED_CODE
41
+ end
42
+
43
+ # Returns true if a command failed.
44
+ def failed?
45
+ !succeed?
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :code
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McProtocolE
4
+ module Frame3e
5
+ # This class shows a access route.
6
+ class AccessRoute
7
+
8
+ OWN_NETWORK_NUM = 0
9
+ OWN_PC_NUM = 255
10
+ NORMAL_UNIT_IO_NUM = 1023
11
+ NORMAL_UNIT_STATION_NUM = 0
12
+
13
+ attr_reader :network_num, :pc_num, :unit_io_num, :unit_station_num
14
+
15
+ # Constructor.
16
+ # @param [Integer] network_num network number
17
+ # @param [Integer] pc_num PC number
18
+ # @param [Integer] unit_io_num unit io number
19
+ # @param [Integer] unit_station_num unit station number
20
+ def initialize(network_num:, pc_num:, unit_io_num:, unit_station_num:)
21
+ @network_num = network_num
22
+ @pc_num = pc_num
23
+ @unit_io_num = unit_io_num
24
+ @unit_station_num = unit_station_num
25
+ end
26
+
27
+ # Returns instance from binary string.
28
+ # @param [String] raw binary string showing a access route
29
+ # @return [AccessRoute] a access route
30
+ def self.from_raw(raw)
31
+ raise ArgumentError, "raw string is not access route" unless raw && raw.size >= 5
32
+
33
+ new(
34
+ network_num: raw[0].unpack1("C"),
35
+ pc_num: raw[1].unpack1("C"),
36
+ unit_io_num: raw[2..3].unpack1("v"),
37
+ unit_station_num: raw[4].unpack1("C"),
38
+ )
39
+ end
40
+
41
+ # Returns instance of own station.
42
+ # @return [AccessRoute] a access route to own station
43
+ def self.own_station
44
+ new(network_num: OWN_NETWORK_NUM, pc_num: OWN_PC_NUM, unit_io_num: NORMAL_UNIT_IO_NUM, unit_station_num: NORMAL_UNIT_STATION_NUM)
45
+ end
46
+
47
+ # Returns binary string.
48
+ # @return [String] binary string
49
+ def to_b
50
+ [
51
+ [network_num].pack("v")[0],
52
+ [pc_num].pack("v")[0],
53
+ [unit_io_num].pack("v"),
54
+ [unit_station_num].pack("v")[0],
55
+ ].join
56
+ end
57
+
58
+ # Returns string.
59
+ # @return [String] string
60
+ def to_s
61
+ "network num: #{network_num} pc num: #{pc_num} unit io num: #{unit_io_num} unit station num: #{unit_station_num}"
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_info'
4
+
5
+ module McProtocolE
6
+ module Frame3e
7
+
8
+ class CommandError < StandardError; end
9
+
10
+ class BaseCommand
11
+
12
+ def initialize(command)
13
+ @command = command
14
+ end
15
+
16
+ def parse(res)
17
+ raise CommandError, ErrorInfo.new(res.code, res.data, self).to_s unless res.succeed?
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :command
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module McProtocolE
6
+ module Frame3e
7
+ # This class shows a command to batch read in word.
8
+ class BatchReadInWord < BaseCommand
9
+
10
+ COMMAND = "\x01\x04\x00\x00".b
11
+
12
+ # Constructor.
13
+ # @param [DeviceRange] device_range device range
14
+ def initialize(device_range:)
15
+ super(COMMAND)
16
+ @device_range = device_range
17
+ end
18
+
19
+ # Returns array of word.
20
+ # @param [Response] res response
21
+ # @return[Array] array of word
22
+ def parse(res)
23
+ super(res)
24
+
25
+ res.data&.each_char&.each_slice(2)&.map(&:join)
26
+ end
27
+
28
+ # Returns binary string.
29
+ # @return [String] binary string
30
+ def to_b
31
+ @to_b ||= command + device_range.to_b
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :device_range
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+
5
+ module McProtocolE
6
+ module Frame3e
7
+ # This class shows a command to batch write in word.
8
+ class BatchWriteInWord < BaseCommand
9
+
10
+ COMMAND = "\x01\x14\x00\x00".b
11
+
12
+ # Constructor.
13
+ # @param [DeviceRange] device_range device range
14
+ # @param [Array<Integer>] values values to write
15
+ # @raise [ArgumentError] when arguments is invalid
16
+ def initialize(device_range:, values:)
17
+ super(COMMAND)
18
+ @device_range = device_range
19
+ @values = values
20
+
21
+ validate
22
+ end
23
+
24
+ # Returns array of word.
25
+ # @param [Response] res response
26
+ # @return[Array] array of word
27
+ def parse(res)
28
+ super(res)
29
+
30
+ res.data&.each_char&.each_slice(2)&.map(&:join)
31
+ end
32
+
33
+ # Returns binary string.
34
+ # @return [String] binary string
35
+ def to_b
36
+ @to_b ||= command + device_range.to_b + values.pack("v*")
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :device_range, :values
42
+
43
+ def validate
44
+ raise ArgumentError, "device points have to equal values size" unless device_range.size == values.size
45
+ raise ArgumentError, "all value have to be an integer" unless values.all? {|value| value.is_a?(Integer) }
46
+
47
+ true
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McProtocolE
4
+ module Frame3e
5
+ # This class shows a device range.
6
+ class DeviceRange
7
+
8
+ module DeviceCode
9
+ DATA_REGISTER = "\xA8".b
10
+ end
11
+
12
+ # Constructor.
13
+ # @param [String] device_code device code
14
+ # @param [Integer] device_num first device number in range
15
+ # @param [Integer] device_points number in range
16
+ def initialize(device_code:, device_num:, device_points:)
17
+ @device_code = device_code
18
+ @device_num = device_num
19
+ @device_points = device_points
20
+ end
21
+
22
+ # Returns device range of data register.
23
+ # @param [Integer] device_num first device number in range
24
+ # @param [Integer] device_points number in range
25
+ def self.data_register(device_num:, device_points:)
26
+ new(device_code: DeviceCode::DATA_REGISTER, device_num: device_num, device_points: device_points)
27
+ end
28
+
29
+ # Returns range size.
30
+ # @return [Integer] range size
31
+ def size
32
+ device_points
33
+ end
34
+
35
+ # Returns binary string.
36
+ # @return [String] binary string
37
+ def to_b
38
+ [[device_num].pack("V")[0..2], device_code, [device_points].pack("v")].join
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :device_code, :device_num, :device_points
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'access_route'
4
+
5
+ module McProtocolE
6
+ module Frame3e
7
+ # This class shows a error information.
8
+ class ErrorInfo
9
+
10
+ # Constructor.
11
+ # @param [Integer] code MC protocol error code
12
+ # @param [String] data response
13
+ # @param [Object] command command
14
+ def initialize(code, data, command)
15
+ @code = code
16
+ @access_route = AccessRoute.from_raw(data[0..4])
17
+ @command = command
18
+ end
19
+
20
+ # Returns binary string.
21
+ # @return [String] binary string
22
+ def to_s
23
+ "code: #{code} command: #{command.class.name} #{access_route}"
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :code, :access_route, :command
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'batch_read_in_word'
4
+ require_relative 'batch_write_in_word'
5
+ require_relative 'response'
6
+
7
+ module McProtocolE
8
+ module Frame3e
9
+ # This class shows a request of MC protocol.
10
+ class Request
11
+
12
+ DEFAULT_READ_TIMEOUT = 3
13
+ SUB_HEADER = "\x50\x00".b
14
+
15
+ # Constructor.
16
+ # @param [AccessRoute] access_route access route
17
+ # @param [Numeric] wait_sec waiting second
18
+ # @param [Object] command command of MC protocol
19
+ def initialize(access_route:, wait_sec:, command:)
20
+ @sub_header = SUB_HEADER
21
+ @access_route = access_route
22
+ @wait_sec = WaitSec.new(wait_sec)
23
+ @command = command
24
+ end
25
+
26
+ # Returns a request to batch read.
27
+ # @param [AccessRoute] access_route access route
28
+ # @param [Numeric] wait_sec waiting second
29
+ # @param [DeviceRange] device_range device_range
30
+ def self.batch_read_in_word(access_route:, wait_sec:, device_range:)
31
+ new(access_route: access_route, wait_sec: wait_sec, command: BatchReadInWord.new(device_range: device_range))
32
+ end
33
+
34
+ # Returns a request to batch write.
35
+ # @param [AccessRoute] access_route access route
36
+ # @param [Numeric] wait_sec waiting second
37
+ # @param [DeviceRange] device_range device_range
38
+ # @param [Array] values values to write
39
+ def self.batch_write_in_word(access_route:, wait_sec:, device_range:, values:)
40
+ new(access_route: access_route, wait_sec: wait_sec, command: BatchWriteInWord.new(device_range: device_range, values: values))
41
+ end
42
+
43
+ # Writes and returns a response.
44
+ # @param [IO] socket TCP socket
45
+ # @param [Integer] read_timeout read timeout second
46
+ # @return [Object] responce
47
+ def exec(socket, read_timeout = DEFAULT_READ_TIMEOUT)
48
+ socket.write(to_b)
49
+ res = Response.recv(socket, read_timeout)
50
+ command.parse(res)
51
+ end
52
+
53
+ # Returns binary string.
54
+ # @return [String] binary string
55
+ def to_b
56
+ sub_header + access_route.to_b + request_len_to_b + wait_sec.to_b + command.to_b
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :sub_header, :access_route, :wait_sec, :command
62
+
63
+ def request_len_to_b
64
+ [wait_sec.to_b.size + command.to_b.size].pack("v")
65
+ end
66
+
67
+ end
68
+
69
+ # This class shows waiting second for MC protocol.
70
+ class WaitSec
71
+
72
+ def initialize(sec)
73
+ @sec = sec
74
+ end
75
+
76
+ def to_b
77
+ return @binary if @binary
78
+
79
+ quarter_sec = (sec * 4).to_i
80
+ @binary = [quarter_sec].pack("v")
81
+ end
82
+
83
+ private
84
+
85
+ attr_reader :sec
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+
5
+ module McProtocolE
6
+ module Frame3e
7
+ # This class shows a responce of MC protocol.
8
+ class Response
9
+
10
+ class TimeoutError < Timeout::Error; end
11
+
12
+ DEFAULT_READ_TIMEOUT = 3
13
+ SUB_HEADER = "\xD0\x00".b
14
+ MAX_RECV_LEN = 1024 * 1024
15
+ SUCCEED_CODE = 0
16
+
17
+ attr_reader :data
18
+
19
+ # Constructor.
20
+ # @param [String] raw_res binary string of response
21
+ def initialize(raw_res)
22
+ @sub_header = raw_res[0..1]
23
+ @access_route = raw_res[2..6]
24
+ @response_len = raw_res[7..8]
25
+ @code = raw_res[9..10].unpack1("v")
26
+ @data = raw_res[11..-1] || ""
27
+ end
28
+
29
+ # Receives responce and returns.
30
+ # @param [IO] socket TCP socket
31
+ # @param [Integer] read_timeout read timeout second
32
+ # @return [Responce] received a response
33
+ def self.recv(socket, read_timeout = DEFAULT_READ_TIMEOUT)
34
+ selected = IO.select([socket], nil, nil, read_timeout)
35
+ raise TimeoutError unless selected
36
+
37
+ raw_res = socket.recv(MAX_RECV_LEN)
38
+ new(raw_res)
39
+ end
40
+
41
+ # Returns true if a command succeed.
42
+ def succeed?
43
+ code == SUCCEED_CODE
44
+ end
45
+
46
+ # Returns true if a command failed.
47
+ def failed?
48
+ !succeed?
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :code
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McProtocolE
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "mc_protocol_e/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "mc_protocol_e"
9
+ spec.version = McProtocolE::VERSION
10
+ spec.authors = ["commis1059"]
11
+ spec.email = ["commis.1059@gmail.com"]
12
+
13
+ spec.summary = %q{A Ruby implementation of a MC protocol client on the ethernet.}
14
+ spec.homepage = "https://github.com/commis1059"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = "https://github.com/commis1059"
24
+ spec.metadata["changelog_uri"] = "https://github.com/commis1059"
25
+ else
26
+ raise "RubyGems 2.0 or newer is required to protect against " \
27
+ "public gem pushes."
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "rspec", "~> 3.0"
42
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mc_protocol_e
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - commis1059
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-28 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: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description:
56
+ email:
57
+ - commis.1059@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".rubocop.yml"
65
+ - ".travis.yml"
66
+ - CODE_OF_CONDUCT.md
67
+ - Gemfile
68
+ - Gemfile.lock
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - bin/console
73
+ - bin/setup
74
+ - lib/mc_protocol_e.rb
75
+ - lib/mc_protocol_e/client.rb
76
+ - lib/mc_protocol_e/frame_1e/access_route.rb
77
+ - lib/mc_protocol_e/frame_1e/batch_read_in_word.rb
78
+ - lib/mc_protocol_e/frame_1e/batch_write_in_word.rb
79
+ - lib/mc_protocol_e/frame_1e/device_range.rb
80
+ - lib/mc_protocol_e/frame_1e/request.rb
81
+ - lib/mc_protocol_e/frame_1e/response.rb
82
+ - lib/mc_protocol_e/frame_3e/access_route.rb
83
+ - lib/mc_protocol_e/frame_3e/base_command.rb
84
+ - lib/mc_protocol_e/frame_3e/batch_read_in_word.rb
85
+ - lib/mc_protocol_e/frame_3e/batch_write_in_word.rb
86
+ - lib/mc_protocol_e/frame_3e/device_range.rb
87
+ - lib/mc_protocol_e/frame_3e/error_info.rb
88
+ - lib/mc_protocol_e/frame_3e/request.rb
89
+ - lib/mc_protocol_e/frame_3e/response.rb
90
+ - lib/mc_protocol_e/version.rb
91
+ - mc_protocol_e.gemspec
92
+ homepage: https://github.com/commis1059
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ homepage_uri: https://github.com/commis1059
97
+ source_code_uri: https://github.com/commis1059
98
+ changelog_uri: https://github.com/commis1059
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.0.3
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: A Ruby implementation of a MC protocol client on the ethernet.
118
+ test_files: []