o3d3xx 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +66 -0
- data/Rakefile +1 -0
- data/bin/o3d3xx-fwupdate.rb +125 -0
- data/lib/o3d3xx.rb +16 -0
- data/lib/o3d3xx/pcic.rb +277 -0
- data/lib/o3d3xx/swupdate.rb +142 -0
- data/lib/o3d3xx/version.rb +15 -0
- data/lib/o3d3xx/xml_rpc_base.rb +71 -0
- data/lib/o3d3xx/xmlrpc.rb +160 -0
- data/o3d3xx.gemspec +24 -0
- metadata +92 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 Christian Ege
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# o3d3xx Ruby library
|
2
|
+
|
3
|
+
Ruby libray and tools for the ifm Efector O3D3xx series of Time of Flight (ToF) Cameras
|
4
|
+
This library adds support for the following interfaces:
|
5
|
+
|
6
|
+
- XML-RPC, provides an interface for camera configuration and set-up
|
7
|
+
- PCIC, provides result data and images
|
8
|
+
- SWUpdate, provides software updates
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'o3d3xx'
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install o3d3xx
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### PCIC Client
|
27
|
+
The PCIC is the propritary process interface which is based on TC/IP
|
28
|
+
|
29
|
+
```
|
30
|
+
pcic = O3D3XX::PCIC.new()
|
31
|
+
pcic.connect('172.25.125.26','50010')
|
32
|
+
pcic.transfer('p0')
|
33
|
+
pcic.async_trigger()
|
34
|
+
```
|
35
|
+
|
36
|
+
### Firmware Update
|
37
|
+
|
38
|
+
This assumes the device is already bootet in swupdate mode
|
39
|
+
|
40
|
+
```
|
41
|
+
o3d3xx-fwupdate.rb -f ~/Downloads/Goldeneye_1.5.205-unstable.swu -I 172.25.125.26 -r
|
42
|
+
```
|
43
|
+
The possible command line options are:
|
44
|
+
```
|
45
|
+
Usage: o3d3xx-fwupdate.rb [options]
|
46
|
+
-f, --file SWU-IMG Image file to upload
|
47
|
+
-r, --reboot Force reboot to productive mode after all other action
|
48
|
+
-I, --ip-addr IP-ADDR Set TCP/IP address of target
|
49
|
+
-p, --start-productive Start productive system only without uploading file
|
50
|
+
-h, --help Display this help message
|
51
|
+
```
|
52
|
+
|
53
|
+
|
54
|
+
## Contributors
|
55
|
+
|
56
|
+
* [Christian Ege](https://github.com/graugans/)
|
57
|
+
* [Daniel Schnell](https://github.com/lumpidu)
|
58
|
+
* [Christoph Freundl](https://github.com/cfreundl)
|
59
|
+
|
60
|
+
## Contributing
|
61
|
+
|
62
|
+
1. Fork it
|
63
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
64
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
65
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
66
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,125 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
3
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
4
|
+
# License:: MIT
|
5
|
+
#
|
6
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
7
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
8
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
9
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
10
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
11
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
12
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
13
|
+
|
14
|
+
|
15
|
+
require 'timeout'
|
16
|
+
require 'optparse'
|
17
|
+
require 'o3d3xx'
|
18
|
+
|
19
|
+
|
20
|
+
def usage(opts)
|
21
|
+
puts opts
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Check sanity of given options
|
26
|
+
def check_options(opts, optparse)
|
27
|
+
rv = false
|
28
|
+
loop do
|
29
|
+
if (! opts[:ip_addr])
|
30
|
+
usage(optparse)
|
31
|
+
puts 'Required argument(s) -I/--ip-addr missing !'
|
32
|
+
break
|
33
|
+
end
|
34
|
+
|
35
|
+
if ((opts[:ip_addr]) && (! (opts[:start_productive] || opts[:file] || opts[:reboot])))
|
36
|
+
usage(optparse)
|
37
|
+
puts 'You have to specify at least one of -p/-f/-r!'
|
38
|
+
break
|
39
|
+
end
|
40
|
+
|
41
|
+
if (((opts[:start_productive]) && (opts[:file] || opts[:reboot]) ))
|
42
|
+
usage(optparse)
|
43
|
+
puts 'Options -p and -r/-f don\'t fit together'
|
44
|
+
break
|
45
|
+
end
|
46
|
+
|
47
|
+
rv = true
|
48
|
+
break
|
49
|
+
end
|
50
|
+
rv
|
51
|
+
end
|
52
|
+
|
53
|
+
options = {}
|
54
|
+
optparse = OptionParser.new do|opts|
|
55
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
|
56
|
+
|
57
|
+
options[:file] = nil
|
58
|
+
opts.on( '-f', '--file SWU-IMG', 'Image file to upload' ) do |file|
|
59
|
+
options[:file] = file
|
60
|
+
end
|
61
|
+
options[:reboot] = false
|
62
|
+
opts.on( '-r', '--reboot', 'Force reboot to productive mode after all other action' ) do
|
63
|
+
options[:reboot] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
options[:ip_addr] = nil
|
67
|
+
opts.on( '-I', '--ip-addr IP-ADDR', 'Set TCP/IP address of target' ) do |ip_addr|
|
68
|
+
options[:ip_addr] = ip_addr
|
69
|
+
end
|
70
|
+
|
71
|
+
options[:start_productive] = false
|
72
|
+
opts.on( '-p', '--start-productive', 'Start productive system only without uploading file' ) do
|
73
|
+
options[:start_productive] = true
|
74
|
+
end
|
75
|
+
|
76
|
+
opts.on( '-h', '--help', 'Display this help message' ) do
|
77
|
+
usage(opts)
|
78
|
+
exit 0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
optparse.parse!(ARGV)
|
83
|
+
|
84
|
+
exit(1) unless check_options(options, optparse)
|
85
|
+
|
86
|
+
ip_addr = options[:ip_addr] || ENV['IP_ADDR'] || raise('Error: no ip address given!')
|
87
|
+
|
88
|
+
swupdate_settings = {
|
89
|
+
:host => ip_addr,
|
90
|
+
:port => 8080,
|
91
|
+
:ssh_ident => options[:identity]
|
92
|
+
}
|
93
|
+
|
94
|
+
swupdate = O3D3XX::Swupdate.new(swupdate_settings)
|
95
|
+
|
96
|
+
rv = 0
|
97
|
+
|
98
|
+
if options[:file]
|
99
|
+
if swupdate.read_status_empty()
|
100
|
+
print "Uploading swu image #{options[:file]} ..."
|
101
|
+
swupdate.upload_file(options[:file])
|
102
|
+
if swupdate.wait_for_status(O3D3XX::Swupdate::UPLOAD_SUCCESS, 60)
|
103
|
+
puts ' OK'
|
104
|
+
rv = 0
|
105
|
+
else
|
106
|
+
puts ' FAILED!'
|
107
|
+
rv = 1
|
108
|
+
end
|
109
|
+
else
|
110
|
+
puts 'Status of http server could not settle!?'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if (options[:start_productive] || options[:reboot]) && (rv == 0)
|
115
|
+
print 'Starting productive mode ...'
|
116
|
+
if swupdate.restart_device()
|
117
|
+
puts ' OK'
|
118
|
+
rv = 0
|
119
|
+
else
|
120
|
+
puts ' FAILED!'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
exit(rv)
|
125
|
+
# EOF
|
data/lib/o3d3xx.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
2
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
3
|
+
# License:: MIT
|
4
|
+
#
|
5
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
6
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
7
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
8
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
9
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
10
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
11
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
12
|
+
|
13
|
+
require "o3d3xx/version"
|
14
|
+
require "o3d3xx/xmlrpc"
|
15
|
+
require "o3d3xx/pcic"
|
16
|
+
require "o3d3xx/swupdate"
|
data/lib/o3d3xx/pcic.rb
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
2
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
3
|
+
# License:: MIT
|
4
|
+
#
|
5
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
6
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
7
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
8
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
9
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
10
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
11
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
require 'socket'
|
16
|
+
require 'timeout'
|
17
|
+
require 'logger'
|
18
|
+
|
19
|
+
module O3D3XX
|
20
|
+
|
21
|
+
CHUNKTYPE = {
|
22
|
+
CT_USERDATA: 0,
|
23
|
+
CT_RADIAL_DISTANCE_IMAGE: 100,
|
24
|
+
CT_AMPLITUDE_IMAGE: 103,
|
25
|
+
CT_INTENSITY_IMAGE: 102,
|
26
|
+
CT_NORM_AMPLITUDE_IMAGE: 101,
|
27
|
+
CT_CARTESIAN_X_COMPONENT: 200,
|
28
|
+
CT_CARTESIAN_Y_COMPONENT: 201,
|
29
|
+
CT_CARTESIAN_Z_COMPONENT: 202,
|
30
|
+
CT_CARTESIAN_ALL: 203,
|
31
|
+
CT_UNIT_VECTOR_E1: 220,
|
32
|
+
CT_UNIT_VECTOR_E2: 221,
|
33
|
+
CT_UNIT_VECTOR_E3: 222,
|
34
|
+
CT_UNIT_VECTOR_ALL: 223,
|
35
|
+
CT_CONFIDENCE_IMAGE: 300,
|
36
|
+
CT_RAWDATA: 301,
|
37
|
+
CT_DIAGNOSTIC: 302,
|
38
|
+
CT_EXTRINSIC_CALIBRATION: 400,
|
39
|
+
CT_JSON_MODEL: 500,
|
40
|
+
}
|
41
|
+
|
42
|
+
CHUNKPIXELFORMAT = {
|
43
|
+
PF_FORMAT_8U: 0,
|
44
|
+
PF_FORMAT_8S: 1,
|
45
|
+
PF_FORMAT_16U: 2,
|
46
|
+
PF_FORMAT_16S: 3,
|
47
|
+
PF_FORMAT_32U: 4,
|
48
|
+
PF_FORMAT_32S: 5,
|
49
|
+
PF_FORMAT_32F: 6,
|
50
|
+
PF_FORMAT_64U: 7,
|
51
|
+
PF_FORMAT_64F: 8,
|
52
|
+
PF_FORMAT_16U2: 9,
|
53
|
+
PF_FORMAT_32F3: 10,
|
54
|
+
PF_FORMAT_12U: 11,
|
55
|
+
}
|
56
|
+
|
57
|
+
class PCIC
|
58
|
+
def initialize()
|
59
|
+
@remote = nil
|
60
|
+
@protocol = 3
|
61
|
+
@logger = Logger.new(STDOUT)
|
62
|
+
@logger.level = Logger::WARN
|
63
|
+
end
|
64
|
+
def connect(host="192.168.0.69",port="50010")
|
65
|
+
unless @remote == nil
|
66
|
+
disconnect()
|
67
|
+
end
|
68
|
+
@remote = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
|
69
|
+
@protocol = 3
|
70
|
+
begin
|
71
|
+
timeout(3) do
|
72
|
+
@remote.connect(Socket.pack_sockaddr_in(port, host))
|
73
|
+
end
|
74
|
+
rescue
|
75
|
+
@logger.fatal("Unable to connect to tcp://#{host}:#{port}")
|
76
|
+
@remote = nil
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
#@protocol = detect_protocol()
|
80
|
+
#set_version(3)
|
81
|
+
end
|
82
|
+
def disconnect()
|
83
|
+
@remote.close()
|
84
|
+
@remote = nil
|
85
|
+
end
|
86
|
+
def detect_protocol()
|
87
|
+
proto = nil
|
88
|
+
begin
|
89
|
+
result = nil
|
90
|
+
@logger.debug("detect_protocol ")
|
91
|
+
begin
|
92
|
+
timeout(11) do
|
93
|
+
send_v3("V?")
|
94
|
+
result = @remote.recvfrom(20)
|
95
|
+
@logger.debug("Received: #{result} ")
|
96
|
+
end
|
97
|
+
rescue Timeout::Error => e
|
98
|
+
@logger.fatal("Timeout while receiving data occurred: #{e.message}")
|
99
|
+
return
|
100
|
+
end
|
101
|
+
test = result.join()
|
102
|
+
proto = nil
|
103
|
+
proto = 1 if test =~ /(\?)|(\d{2} \d{2} \d{2})\r\n/
|
104
|
+
proto = 2 if test =~ /\d{4}(\?)|(\d{2} \d{2} \d{2})\r\n/
|
105
|
+
proto = 3 if test =~ /\d{4}L\d{9}\r\n\d{4}/
|
106
|
+
proto = 4 if test =~ /L\d{9}\r\n(\?)|(\d{2} \d{2} \d{2})\r\n/
|
107
|
+
# If the device is in continuous mode and asynchronous messages are enabled
|
108
|
+
# we have to pull the complete frame out of the socket. Otherwise every consecutive
|
109
|
+
# PCIC call will fail due to the fact that we start in the middle of a frame.
|
110
|
+
if(proto == 3 )
|
111
|
+
ticket,length = test.match(/(\d{4})L(\d{9})\r\n/i).captures
|
112
|
+
timeout(5) do
|
113
|
+
# get the rest of the response
|
114
|
+
trailer = @remote.read(length.to_i-4)
|
115
|
+
end
|
116
|
+
# recv_v3 handles asynchronous messages so let it handle them.
|
117
|
+
dum = recv_v3() if '0000' == ticket
|
118
|
+
end
|
119
|
+
raise 'Unable to detect PCIC protocol' if proto == nil
|
120
|
+
rescue Exception => e
|
121
|
+
puts e.message
|
122
|
+
puts e.backtrace.inspect
|
123
|
+
raise('Timeout while detecting version')
|
124
|
+
proto = nil
|
125
|
+
end
|
126
|
+
proto
|
127
|
+
end
|
128
|
+
def set_version(version=3)
|
129
|
+
res = transfer("v%02d"%version)
|
130
|
+
@protocol = version
|
131
|
+
res
|
132
|
+
end
|
133
|
+
def version()
|
134
|
+
return transfer("V?")
|
135
|
+
end
|
136
|
+
def help()
|
137
|
+
transfer("H?")
|
138
|
+
end
|
139
|
+
def device_info()
|
140
|
+
transfer("G?")
|
141
|
+
end
|
142
|
+
def async_trigger()
|
143
|
+
return transfer("t")
|
144
|
+
end
|
145
|
+
def sync_trigger()
|
146
|
+
transfer("T?")
|
147
|
+
end
|
148
|
+
|
149
|
+
def get_lastimage(img = 3)
|
150
|
+
transfer("I%02d?"%img)
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
def async_result(state=false)
|
155
|
+
data = "p%d" % [ state ? 1 : 0 ]
|
156
|
+
res = true
|
157
|
+
transfer(data)
|
158
|
+
res
|
159
|
+
end
|
160
|
+
def transfer(data)
|
161
|
+
raise 'Protocol #{@protocol} currently not supported' if @protocol == 1
|
162
|
+
raise 'Protocol #{@protocol} currently not supported' if @protocol == 2
|
163
|
+
return transfer_v3(data) if @protocol == 3
|
164
|
+
raise 'Protocol #{@protocol} currently not supported' if @protocol == 4
|
165
|
+
end
|
166
|
+
def send_v3(data)
|
167
|
+
ticket = "%04d" % [1000+rand(8999)]
|
168
|
+
length = "%09d" % [data.bytesize+6]
|
169
|
+
sendbuf = "#{ticket}L#{length}\r\n#{ticket}#{data}\r\n"
|
170
|
+
@logger.info("Request (#{@protocol}) : %{sendbuf}" % {:sendbuf => escape_pcic_string(sendbuf)})
|
171
|
+
timeout(5) do
|
172
|
+
@remote.send(sendbuf,0)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
def transfer_v3(data,omit_async=true)
|
176
|
+
send_v3(data)
|
177
|
+
continue = true
|
178
|
+
response = nil
|
179
|
+
begin
|
180
|
+
async,ticket,response = recv_v3()
|
181
|
+
continue = false; continue = true if async and omit_async
|
182
|
+
end while continue
|
183
|
+
return response
|
184
|
+
end
|
185
|
+
|
186
|
+
def recv_v3()
|
187
|
+
data = nil
|
188
|
+
async = false
|
189
|
+
trailer = nil
|
190
|
+
# The V3 response shall look like this
|
191
|
+
# <4 Byte ticket >L<9 Byte Length in dec>\r\n<4 Byte ticket ><Response>\r\n
|
192
|
+
# receive <ticket>L<length>\r\n to check if we in sync and how much
|
193
|
+
# data we have to receive in the next step
|
194
|
+
timeout(5) do
|
195
|
+
data = @remote.read(16)
|
196
|
+
end
|
197
|
+
@logger.info("Header : %{res}" % {:res => escape_pcic_string(data)})
|
198
|
+
begin
|
199
|
+
ticket,length = data.match(/(\d{4})L(\d{9})\r\n/i).captures
|
200
|
+
rescue
|
201
|
+
@logger.fatal(">>> rescue: #{data} <<<")
|
202
|
+
end
|
203
|
+
size = length.to_i
|
204
|
+
@logger.info("Length : %d " % size )
|
205
|
+
if ticket == "0000"
|
206
|
+
@logger.info('ASYNC reply Caught !!!!')
|
207
|
+
async = true
|
208
|
+
else
|
209
|
+
async = false
|
210
|
+
end
|
211
|
+
timeout(5) do
|
212
|
+
trailer = @remote.read(size)
|
213
|
+
end
|
214
|
+
if size < 80 and trailer.length < 80
|
215
|
+
@logger.debug("Trailer : %{res} <<<<" % {:res => escape_pcic_string(trailer)})
|
216
|
+
else
|
217
|
+
escaped = escape_pcic_string(trailer)
|
218
|
+
@logger.debug("Trailer : %{start} [...] %{stop}" % {:start => escaped[0..20], :stop => escaped[-20..-1]})
|
219
|
+
end
|
220
|
+
ticket_trailer = trailer[0..3]
|
221
|
+
unless ticket_trailer == ticket
|
222
|
+
raise "Ticket mismatch. Header ticket: #{ticket} trailer ticket: #{ticket_trailer}"
|
223
|
+
end
|
224
|
+
# remove ticket and trailing \r\n
|
225
|
+
return async,ticket,trailer[4..-3]
|
226
|
+
end
|
227
|
+
|
228
|
+
def escape_pcic_string(data)
|
229
|
+
result = ""
|
230
|
+
result = data.gsub("\n","\\\\n").gsub("\r","\\\\r") unless data == nil
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Handle PCIC binary/image chunks
|
235
|
+
class Chunk
|
236
|
+
def self.parse(data)
|
237
|
+
chunk_header = data.unpack('L<9*')
|
238
|
+
result = {
|
239
|
+
:CHUNK_TYPE => chunk_header[0],
|
240
|
+
:CHUNK_SIZE => chunk_header[1],
|
241
|
+
:HEADER_SIZE => chunk_header[2],
|
242
|
+
:HEADER_VERSION => chunk_header[3],
|
243
|
+
:IMAGE_WIDTH => chunk_header[4],
|
244
|
+
:IMAGE_HEIGTH => chunk_header[5],
|
245
|
+
:PIXEL_FORMAT => chunk_header[6],
|
246
|
+
:TIME_STAMP => chunk_header[7],
|
247
|
+
:FRAME_COUNT => chunk_header[8],
|
248
|
+
:PIXEL_DATA => data[chunk_header[2]...chunk_header[1]],
|
249
|
+
}
|
250
|
+
end
|
251
|
+
def self.info(chunk)
|
252
|
+
info = Array.new
|
253
|
+
info.push("CHUNK type : #{chunk[:CHUNK_TYPE]}")
|
254
|
+
info.push("CHUNK size : #{chunk[:CHUNK_SIZE]}")
|
255
|
+
info.push("CHUNK header size : #{chunk[:HEADER_SIZE]}")
|
256
|
+
info.push("CHUNK version : #{chunk[:HEADER_VERSION]}")
|
257
|
+
info.push("CHUNK height : #{chunk[:IMAGE_HEIGTH]}")
|
258
|
+
info.push("CHUNK width : #{chunk[:IMAGE_WIDTH]}")
|
259
|
+
info.push("CHUNK format : #{chunk[:PIXEL_FORMAT]}")
|
260
|
+
info.push("CHUNK time stamp : #{chunk[:TIME_STAMP]}")
|
261
|
+
info.push("CHUNK frame count : #{chunk[:FRAME_COUNT]}")
|
262
|
+
info.push("CHUNK data size : #{chunk[:PIXEL_DATA].length}")
|
263
|
+
end
|
264
|
+
|
265
|
+
def self.parseChunkArray(data)
|
266
|
+
result = Array.new
|
267
|
+
offset = 0
|
268
|
+
while offset < data.size
|
269
|
+
chunk = self.parse(data[offset..data.size])
|
270
|
+
result << chunk
|
271
|
+
offset += chunk[:CHUNK_SIZE]
|
272
|
+
end
|
273
|
+
return result
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
|
2
|
+
# The module provides access to the swupdate process
|
3
|
+
#
|
4
|
+
# swupdate - Software Update for Embedded Systems provides a web ui to upload
|
5
|
+
# an installation file to an embedded target. This can also be used for an automated
|
6
|
+
# installation process.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# For manual access an web ui is provided
|
10
|
+
# Web-Browser: http://<ip of the device>:8080
|
11
|
+
#
|
12
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
13
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
14
|
+
# License:: MIT
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
|
25
|
+
require 'timeout'
|
26
|
+
require 'uri'
|
27
|
+
require 'net/http'
|
28
|
+
require 'json'
|
29
|
+
|
30
|
+
module O3D3XX
|
31
|
+
|
32
|
+
class Swupdate
|
33
|
+
UPLOAD_SUCCESS={"Status"=>"3", "Msg"=>"SWUPDATE successful !", "Error"=>"0"}
|
34
|
+
|
35
|
+
def initialize (settings={})
|
36
|
+
raise 'No host name given !' if settings[:host].nil?
|
37
|
+
@config = {
|
38
|
+
:host => settings[:host],
|
39
|
+
}
|
40
|
+
if settings[:port].nil?
|
41
|
+
@config[:port] = 8080
|
42
|
+
else
|
43
|
+
@config[:port] = settings[:port]
|
44
|
+
end
|
45
|
+
@base_uri = "http://#{@config[:host]}:#{@config[:port]}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Uploads a file to swupdate system. So far this file has
|
49
|
+
# to be a swu image file.
|
50
|
+
# This call is asynchronous to the following installation
|
51
|
+
# procedure, i.e. one has to poll for installation finish
|
52
|
+
# via query_status()
|
53
|
+
#
|
54
|
+
# @param filename filename of swu image to install on target
|
55
|
+
#
|
56
|
+
def upload_file(filename)
|
57
|
+
raise 'Invalid file name given !' unless File.exist?(filename)
|
58
|
+
uri = URI.parse("#{@base_uri}/handle_post_request")
|
59
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
60
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
61
|
+
request.body = File.read(filename)
|
62
|
+
request['Content-Type'] = 'application/octet-stream'
|
63
|
+
request['Connection'] = 'keep-alive'
|
64
|
+
request['X_FILENAME'] = "#{File.basename(filename)}"
|
65
|
+
http.request(request)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Reads status queue empty on http server, i.e.
|
69
|
+
# will return status from server until two consecutive
|
70
|
+
# status values are identical.
|
71
|
+
#
|
72
|
+
# @param timeout Time to wait for http server to settle
|
73
|
+
#
|
74
|
+
def read_status_empty(timeout = 5)
|
75
|
+
rv = false
|
76
|
+
Timeout::timeout(timeout) do
|
77
|
+
loop do
|
78
|
+
rv1 = query_status
|
79
|
+
rv2 = query_status
|
80
|
+
break if rv1 == rv2
|
81
|
+
end
|
82
|
+
rv = true
|
83
|
+
end
|
84
|
+
rv
|
85
|
+
end
|
86
|
+
|
87
|
+
# Query status from http server
|
88
|
+
#
|
89
|
+
# @return json representation of http status
|
90
|
+
# e.g. {"Status"=>"0", "Msg"=>"", "Error"=>"0"}
|
91
|
+
#
|
92
|
+
def query_status()
|
93
|
+
rv = ''
|
94
|
+
uri = URI.parse("#{@base_uri}/getstatus.json")
|
95
|
+
begin
|
96
|
+
response = Net::HTTP.get_response(uri)
|
97
|
+
rv = JSON.parse(response.body)
|
98
|
+
#Net::HTTP.get_print(uri)
|
99
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError,
|
100
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
101
|
+
end
|
102
|
+
#puts rv
|
103
|
+
rv
|
104
|
+
end
|
105
|
+
|
106
|
+
# Waits for specific status of http server with max. timeout
|
107
|
+
#
|
108
|
+
# @param status_hash Hash of status values, e.g.
|
109
|
+
# {"Status"=>"0", "Msg"=>"", "Error"=>"0"}
|
110
|
+
#
|
111
|
+
# @param timeout Time in seconds to wait for given status to be returned
|
112
|
+
# from http server
|
113
|
+
#
|
114
|
+
def wait_for_status(status_hash, timeout)
|
115
|
+
rv = false
|
116
|
+
Timeout::timeout(timeout) do
|
117
|
+
loop do
|
118
|
+
rv1 = query_status
|
119
|
+
break if rv1 == status_hash
|
120
|
+
# Print error messages
|
121
|
+
if rv1['Status'] && (rv1['Status'] == '4')
|
122
|
+
puts rv1['Msg'] if rv1['Msg']
|
123
|
+
end
|
124
|
+
sleep 1
|
125
|
+
end
|
126
|
+
rv = true
|
127
|
+
end
|
128
|
+
rv
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Restarts device
|
133
|
+
def restart_device()
|
134
|
+
uri = URI.parse("#{@base_uri}/reboot_to_live")
|
135
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
136
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
137
|
+
request['Connection'] = 'keep-alive'
|
138
|
+
http.request(request)
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
2
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
3
|
+
# License:: MIT
|
4
|
+
#
|
5
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
6
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
7
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
8
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
9
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
10
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
11
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
12
|
+
|
13
|
+
module O3D3XX
|
14
|
+
VERSION = "0.0.1"
|
15
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
2
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
3
|
+
# License:: MIT
|
4
|
+
#
|
5
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
6
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
7
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
8
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
9
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
10
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
11
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
12
|
+
|
13
|
+
|
14
|
+
require 'xmlrpc/client'
|
15
|
+
require 'timeout'
|
16
|
+
|
17
|
+
class XmlRPCBase
|
18
|
+
|
19
|
+
attr_reader :rpc_cl
|
20
|
+
|
21
|
+
# Initializes XMLRPC connection. Needs connection and
|
22
|
+
# Request parameters via given settings hash
|
23
|
+
#
|
24
|
+
# @param settings hash of settings: :host, :port, :path
|
25
|
+
#
|
26
|
+
def initialize(settings={})
|
27
|
+
raise 'No host name given !' if settings[:host].nil?
|
28
|
+
raise 'No port given !' if settings[:port].nil?
|
29
|
+
raise 'No endpoint given !' if settings[:path].nil?
|
30
|
+
@config = {
|
31
|
+
:host => settings[:host],
|
32
|
+
:port => settings[:port],
|
33
|
+
:path => '/api/rpc/v1/' + settings[:path]
|
34
|
+
}
|
35
|
+
|
36
|
+
@rpc_cl = XMLRPC::Client.new(@config[:host],
|
37
|
+
@config[:path],
|
38
|
+
@config[:port], nil, nil,
|
39
|
+
nil, nil, nil, 20) # 20 seconds connection timeout
|
40
|
+
#dump
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Calls any method via xmlrpc
|
45
|
+
def method_missing(meth, *args)
|
46
|
+
arg = args
|
47
|
+
begin
|
48
|
+
@rpc_cl.call_async(meth.to_s, *arg)
|
49
|
+
rescue XMLRPC::FaultException => e
|
50
|
+
if e.message.include? 'method not found'
|
51
|
+
super
|
52
|
+
else
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Returns configuration object
|
60
|
+
def getConfig
|
61
|
+
return @config
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Dumps configuration
|
66
|
+
def dump
|
67
|
+
puts "Host: #{@config[:host]}"
|
68
|
+
puts "Port: #{@config[:port]}"
|
69
|
+
puts "URL : #{@config[:path]}"
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# The module provides a transparent XML-RPC proxy for access to the O3D3XX
|
2
|
+
# class of devices.
|
3
|
+
# To get an rough overview of the API connect to the device using your preferred
|
4
|
+
# Web-Browser: http://192.168.0.69/api/rpc/v1/com.ifm.efector/
|
5
|
+
#
|
6
|
+
# Author:: Christian Ege (mailto:k4230r6@gmail.com)
|
7
|
+
# Copyright:: Copyright (c) 2014 - 2016
|
8
|
+
# License:: MIT
|
9
|
+
#
|
10
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
11
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
12
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
13
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
14
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
15
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
16
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
|
18
|
+
|
19
|
+
require 'o3d3xx/xml_rpc_base'
|
20
|
+
|
21
|
+
module O3D3XX
|
22
|
+
|
23
|
+
# This class encapsulates the XML-RPC network configuration settings
|
24
|
+
class NetworkProxy < XmlRPCBase
|
25
|
+
def initialize(proxy)
|
26
|
+
config = proxy.getConfig()
|
27
|
+
path = "com.ifm.efector/session_#{proxy.getSessionID()}/edit/device/network/"
|
28
|
+
super(:host => config[:host], :port => config[:port], :path => path)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Application
|
33
|
+
TriggerMode = {
|
34
|
+
:FREE_RUN => 1,
|
35
|
+
:PCIC => 2,
|
36
|
+
:POSITIVE_EDGE => 3,
|
37
|
+
:NEGATIVE_EDGE => 4,
|
38
|
+
:BOTH_EDGE => 5,
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# This class encapsulates the XML-RPC imager access
|
43
|
+
class ImagerProxy < XmlRPCBase
|
44
|
+
def initialize(proxy)
|
45
|
+
config = proxy.getConfig()
|
46
|
+
path = "com.ifm.efector/session_#{proxy.getSessionID()}/edit/application/imager_001" # On O3D3xx only imager_001 is available
|
47
|
+
super(:host => config[:host], :port => config[:port], :path => path)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# This class encapsulates the XML-RPC application access
|
53
|
+
class ApplicationProxy < XmlRPCBase
|
54
|
+
def initialize(proxy)
|
55
|
+
config = proxy.getConfig()
|
56
|
+
path = "com.ifm.efector/session_#{proxy.getSessionID()}/edit/application/"
|
57
|
+
super(:host => config[:host], :port => config[:port], :path => path)
|
58
|
+
@img_proxy = O3D3xx::ImagerProxy.new(proxy)
|
59
|
+
end
|
60
|
+
|
61
|
+
def getImagerProxy()
|
62
|
+
@img_proxy
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
# This class encapsulates the XML-RPC device access
|
68
|
+
class DeviceProxy < XmlRPCBase
|
69
|
+
def initialize(proxy)
|
70
|
+
config = proxy.getConfig()
|
71
|
+
path = "com.ifm.efector/session_#{proxy.getSessionID()}/edit/device/"
|
72
|
+
super(:host => config[:host], :port => config[:port], :path => path)
|
73
|
+
@net_proxy = O3D3xx::NetworkProxy.new(proxy)
|
74
|
+
end
|
75
|
+
|
76
|
+
def getNetworkProxy()
|
77
|
+
@net_proxy
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# This class encapsulates the XML-RPC Edit-Mode access
|
82
|
+
class EditProxy < XmlRPCBase
|
83
|
+
def initialize(proxy)
|
84
|
+
config = proxy.getConfig()
|
85
|
+
path = "com.ifm.efector/session_#{proxy.getSessionID()}/edit/"
|
86
|
+
super(:host => config[:host], :port => config[:port], :path => path)
|
87
|
+
@proxy = proxy
|
88
|
+
@device = O3D3xx::DeviceProxy.new(@proxy)
|
89
|
+
@application = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def getApplicationProxy(index)
|
93
|
+
self.editApplication(index)
|
94
|
+
@application = O3D3xx::ApplicationProxy.new(@proxy)
|
95
|
+
end
|
96
|
+
|
97
|
+
def closeApplication()
|
98
|
+
self.stopEditingApplication() unless @application == nil
|
99
|
+
@application = nil
|
100
|
+
end
|
101
|
+
|
102
|
+
def getDeviceProxy()
|
103
|
+
@device
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
module Session
|
108
|
+
OperationMode = {
|
109
|
+
:RUN => 0,
|
110
|
+
:EDIT => 1,
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
# This class encapsulates the XML-RPC session access
|
116
|
+
class SessionProxy < XmlRPCBase
|
117
|
+
def initialize(proxy)
|
118
|
+
config = proxy.getConfig()
|
119
|
+
path = "com.ifm.efector/session_#{proxy.getSessionID()}/"
|
120
|
+
super(:host => config[:host], :port => config[:port], :path => path)
|
121
|
+
@edit = nil
|
122
|
+
@proxy = proxy
|
123
|
+
end
|
124
|
+
|
125
|
+
def getEditObjectProxy()
|
126
|
+
if nil == @edit
|
127
|
+
self.setOperatingMode(O3D3xx::Session::OperationMode[:EDIT])
|
128
|
+
@edit = O3D3xx::EditProxy.new(@proxy)
|
129
|
+
end
|
130
|
+
@edit
|
131
|
+
end
|
132
|
+
def closeEdit()
|
133
|
+
self.setOperatingMode(O3D3xx::Session::OperationMode[:RUN]) unless @edit == nil
|
134
|
+
@edit = nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# This class encapsulates the XML-RPC general o3d3xx access
|
139
|
+
class O3D3xxProxy < XmlRPCBase
|
140
|
+
|
141
|
+
def initialize(host='192.168.0.69', port=80)
|
142
|
+
super(:host => host, :port => port, :path => 'com.ifm.efector/')
|
143
|
+
@session = nil
|
144
|
+
end
|
145
|
+
|
146
|
+
def getSessionProxy(*args)
|
147
|
+
@session_id = requestSession(*args)
|
148
|
+
@session = O3D3xx::SessionProxy.new(self)
|
149
|
+
end
|
150
|
+
def closeSession()
|
151
|
+
@session_id = nil
|
152
|
+
@session.cancelSession() unless @session == nil
|
153
|
+
@session = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def getSessionID
|
157
|
+
return @session_id
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/o3d3xx.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'o3d3xx/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "o3d3xx"
|
8
|
+
spec.version = O3D3XX::VERSION
|
9
|
+
spec.authors = ["Christian Ege"]
|
10
|
+
spec.email = ["k4230r6@gmail.com"]
|
11
|
+
spec.description = "Ruby interface for ifm efector O3d3xx"
|
12
|
+
spec.summary = "Ruby interface for ifm efector O3d3xx"
|
13
|
+
spec.homepage = "https://github.com/graugans/ruby-o3d3xx"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.required_ruby_version = ">= 1.9.3"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: o3d3xx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Christian Ege
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-03-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Ruby interface for ifm efector O3d3xx
|
47
|
+
email:
|
48
|
+
- k4230r6@gmail.com
|
49
|
+
executables:
|
50
|
+
- o3d3xx-fwupdate.rb
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- bin/o3d3xx-fwupdate.rb
|
60
|
+
- lib/o3d3xx.rb
|
61
|
+
- lib/o3d3xx/pcic.rb
|
62
|
+
- lib/o3d3xx/swupdate.rb
|
63
|
+
- lib/o3d3xx/version.rb
|
64
|
+
- lib/o3d3xx/xml_rpc_base.rb
|
65
|
+
- lib/o3d3xx/xmlrpc.rb
|
66
|
+
- o3d3xx.gemspec
|
67
|
+
homepage: https://github.com/graugans/ruby-o3d3xx
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 1.9.3
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.8.23
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Ruby interface for ifm efector O3d3xx
|
92
|
+
test_files: []
|