gm3156 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 15bff36286b3a2400520c9c60c84127385383c6c1ec0de527623d293e8ddafae
4
+ data.tar.gz: 67484565194ee5906d774ed712693f16c064b5b10fd789421b5d29dc6c0bbb96
5
+ SHA512:
6
+ metadata.gz: 2dda4bbb3f0ab63ef8cd6418c80b9bd1ccfbf9afe2286624aa2ec391cb7a9a8387f5c1e2236be567c4df92b380139079e989e7c95958ae799230330eb80ce6e1
7
+ data.tar.gz: f1192b16b8ca6d3fbc6003d2af7776a96f65e38c3bda37d74018d075729ea65eb0b0c27014ffa4e107a54ec46adfd5eb04571d46adc3266702cdbd3065a9cdf7
@@ -0,0 +1,76 @@
1
+ # GM1356 digital sound level meter USB protocol description
2
+
3
+ ## Interface description
4
+
5
+ Interface is recognised as a **HID** with **two endpoints**. One **in** and one **out**. Both work via **URB interruptions** with **8 bytes packet data length**.
6
+
7
+ ## Ask for current state
8
+
9
+ To ask for current state, send `URB_INTERRUPT out` to endpoint **2** with capture data `b3[random_magic_id]:24bits]00000000`. Then you will receive `URB_INTERRUPT in` from endpoint **1** with data described below.
10
+
11
+ It looks that SoundLab generates some random (or not random) id per each instance of application. First I tried to use exactly the same requests in my driver and I received no response. It means the device has to store it somewhere and prevents from using it again. No idea why. After generating `random_magic_id` it started working.
12
+
13
+ You can use the same `random_magic_id` per instance of your application as SoundLab do.
14
+
15
+ ###example capture data
16
+ ``
17
+
18
+ ## Decode current state
19
+
20
+ `URB_INTERRUPT in` have leftover capture data with this structure:
21
+ `[value:16bits][settings:4bits][range:4bits][unknown:40bits]` - 64bits
22
+
23
+ ### Decode sound level `value`
24
+ Convert 16bits to decimal, then add point before last digit.
25
+
26
+ ### Decode `settings`
27
+ Settings have this structure:
28
+ `[unused:1bit][slow/fast mode:1bit][max mode:1bit][a/c filter:1bit]` - 4 bits
29
+
30
+ ```
31
+ a, not max, slow | 0
32
+ c, not max, slow | 1
33
+ a, max, slow | 2
34
+ c, max, slow | 3
35
+ a, not max, fast | 4
36
+ c, not max, fast | 5
37
+ a, max, fast | 6
38
+ c, max, fast | 7
39
+ ```
40
+
41
+ ### Decode `range`
42
+ Convert 4bits to decimal. Use decimal to assign corresponding range.
43
+
44
+ ```
45
+ 30-130 | 0
46
+ 30-60 | 1
47
+ 50-100 | 2
48
+ 60-110 | 3
49
+ 80-130 | 4
50
+ ```
51
+
52
+ ### Example
53
+ #### data:
54
+ `0292749b90ddc0ff`
55
+ #### meaning:
56
+ * `value`:
57
+ * `0292` is `658` decimal, add point before last digit and it is `65.8`dB
58
+ * `settings`:
59
+ * `7` is `0111` binary, so it is `fast mode`, `max mode`, `filter c`
60
+ * `range`:
61
+ * `4` means `80-130`dB range
62
+
63
+ ## Change settings and range
64
+
65
+ To change settings send `INTERRUPT out` to endpoint **2** with such format:
66
+
67
+ ### Data format
68
+ `56[settings:4bits][range:4bits]000000000000`
69
+
70
+ `settings` and `range` are the same as described in previous section.
71
+
72
+ ### Example
73
+ #### data:
74
+ `5621000000000000`
75
+ #### meaning:
76
+ Set state to `a filter`, `max mode`, `slow mode` with `30-60`dB range.
@@ -0,0 +1,38 @@
1
+ # GM1356 for Linux
2
+
3
+ ## Description
4
+ This driver was written for **Digital Sound Level Meter** with USB (type **GM1356**) serial number: `HA:1303162` ordered from China via Aliexpress. My sonometer works with **SoundLab** `Sound Level Meter v. 1.0.0.20, build 2016-07-20` delivered by [Benetech Poland](https://benetech-poland.pl/) (thank you very much for this). I was trying to run it with SoundLab downloaded from Bogen website, but it couldn't connect to the device. It means my driver may not work with some GM1356 devices.
5
+
6
+ ## Installation
7
+ After cloning this git repository make sure you have `ruby` interpretter and `bundler` gem. Then go to the main directory and run:
8
+
9
+ ```bundle install```
10
+
11
+ Now make sure that your `users` have access to your device. In my case I had to create `/etc/udev/rules.d/90-usbpermission.rules` with such content:
12
+
13
+ ```SUBSYSTEMS=="usb",ATTRS{idProduct}=="74e3",ATTRS{idVendor}=="64bd",GROUP="users",MODE="0666"```
14
+
15
+ Then run:
16
+
17
+ ```sudo udevadm control --reload```
18
+
19
+ And after all reconnect your device if it was already connected.
20
+
21
+ ## Usage
22
+ If you want to print real time data from your sonometer, run:
23
+
24
+ ```./bin/gm3156```
25
+
26
+ For more options:
27
+
28
+ ```./bin/gm3156 --help```
29
+
30
+ ## Protocol
31
+ I used Wireshark with USBPcap to sniff communication between my device and SoundLab on Windows in order to understend the protocol. Protocol is described in [PROTOCOL.md](PROTOCOL.md).
32
+
33
+ ## Supported functionalities
34
+ * receiving real time data with `sound level value`, `A/C filter`, `max mode`, `slow/fast mode` and `measured range`
35
+ * changing `A/C filter`, `max mode`, `slow/fast mode` and `measured range`
36
+
37
+ ## Unsupported functionalities
38
+ * importing recorded data
data/TODO.md ADDED
@@ -0,0 +1,7 @@
1
+ * figure out why some packets are invalid and handle it properly instead of retrying
2
+ * add editing settings feature
3
+ * write help for `./bin/gm3156`
4
+ * write api documentation
5
+ * create gem
6
+ * handle reconnecting device / unconnected device etc. exceptions
7
+ * test if it's stable
@@ -0,0 +1,51 @@
1
+ #!ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative '../lib/gm3156'
6
+
7
+ options = {}
8
+
9
+ OptionParser.new do |opts|
10
+ opts.banner = 'Usage: gm3156 [options]'
11
+
12
+ opts.on('-fFILTER', '--filter=FILTER', String, 'A/C filter') do |f|
13
+ options[:filter] = :a if f.downcase == 'a'
14
+ options[:filter] = :c if f.downcase == 'c'
15
+ end
16
+
17
+ opts.on('-sSPEED', '--speed=SPEED', String, 'SLOW/FAST mode') do |s|
18
+ options[:speed] = :slow if ['slow', 's'].include? s.downcase
19
+ options[:speed] = :fast if ['fast', 'f'].include? s.downcase
20
+ end
21
+
22
+ opts.on('-mBOOLEAN', '--max=BOOLEAN', TrueClass, 'enable/disable MAX mode') do |m|
23
+ options[:max_mode] = m
24
+ end
25
+
26
+ opts.on('-rINTEGER', '--range=INTEGER', Integer, 'measured levels range ') do |l|
27
+ options[:max_mode] = l if (0..4).include? l
28
+ end
29
+
30
+ opts.on('--just-set', TrueClass, 'just set options and exit') do
31
+ options[:just_set] = true
32
+ end
33
+
34
+ opts.on("-h", "--help", "Prints this help") do
35
+ puts opts
36
+ exit
37
+ end
38
+ end.parse!
39
+
40
+ device = GM3156::Device.new(options)
41
+
42
+ begin
43
+ device.read do |record|
44
+ puts record.to_s
45
+ end
46
+ rescue Exception # rubocop:disable Lint/RescueException
47
+ device.close
48
+ raise
49
+ end unless options[:just_set]
50
+
51
+ device.close
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GM3156
4
+ require_relative 'gm3156/errors'
5
+ require_relative 'gm3156/device'
6
+ require_relative 'gm3156/record'
7
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GM3156
4
+ require 'hidapi'
5
+ require_relative 'errors'
6
+ require_relative 'settings'
7
+ require_relative 'record'
8
+
9
+ class Device
10
+ STATE_REQUEST = [0xb3, Random.rand(256), Random.rand(256), Random.rand(256), 0, 0, 0, 0].pack('C*')
11
+
12
+ def initialize(vendor_id = 0x64bd, product_id = 0x74e3, **args)
13
+ self.vendor_id = vendor_id
14
+ self.product_id = product_id
15
+ self.device = HIDAPI.open(vendor_id, product_id)
16
+ read_current_state
17
+ set_settings(**args) unless args.empty?
18
+ end
19
+
20
+ def close
21
+ device.close
22
+ end
23
+
24
+ def read(&block)
25
+ loop do
26
+ Thread.new do
27
+ read_current_state(&block)
28
+ end
29
+ sleep settings.speed == :fast? ? 0.5 : 1
30
+ end
31
+ end
32
+
33
+ def set_settings(**args)
34
+ args.each do |name, value|
35
+ settings.send("#{name}=", value) if settings.respond_to?("#{name}=")
36
+ end
37
+ data = [0x56, settings.pack, 0, 0, 0, 0, 0, 0].pack('C*')
38
+
39
+ device.write(data)
40
+ read_cap_data
41
+ sleep(0.1)
42
+ end
43
+
44
+ private
45
+
46
+ def read_current_state(&block)
47
+ begin
48
+ send_state_request
49
+ data = read_cap_data
50
+
51
+ raise InvalidCaptureDataLengthError.new('Capture data should have 8 bytes.', data) if data.length != 8
52
+
53
+ record = Record.new(data.unpack('H*').first)
54
+ self.settings = record.settings
55
+ block_given? ? block.call(record) : record
56
+ rescue InvalidCaptureDataLengthError
57
+ sleep 0.1
58
+ retry
59
+ end
60
+ end
61
+
62
+ def send_state_request
63
+ device.write(STATE_REQUEST)
64
+ end
65
+
66
+ def read_cap_data
67
+ cap_data = nil
68
+ cap_data = device.read_timeout(1000) until cap_data && !cap_data.empty?
69
+ cap_data
70
+ end
71
+
72
+ attr_accessor :vendor_id, :product_id, :device, :settings
73
+ end
74
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GM3156
4
+ class InvalidCaptureDataLengthError < StandardError
5
+ attr_reader :data
6
+
7
+ def initialize(msg, data)
8
+ @data = data
9
+ super(msg)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GM3156
4
+ require_relative 'settings'
5
+
6
+ class Record
7
+ def initialize(message)
8
+ self.timestamp = Time.now
9
+ self.spl = extract_spl(message)
10
+ self.settings = Settings.new(message)
11
+ end
12
+
13
+ def extract_spl(message)
14
+ message[0..3].to_i(16).to_s.insert(-2, '.').to_f
15
+ end
16
+
17
+ def to_s
18
+ output = "#{timestamp.strftime('%H:%M:%S')} \t SPL: #{spl}dB#{settings.filter.to_s.capitalize}"
19
+ output += "\t MAX mode" if settings.max_mode
20
+ output
21
+ end
22
+
23
+ attr_reader :timestamp, :spl, :settings
24
+
25
+ private
26
+
27
+ attr_writer :timestamp, :spl, :settings
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GM3156
4
+ class Settings
5
+ AVAILABLE_RANGES = [
6
+ '30-120',
7
+ '30-60',
8
+ '50-100',
9
+ '60-110',
10
+ '80-130'
11
+ ]
12
+
13
+ def initialize(message)
14
+ assign_settings(message)
15
+ end
16
+
17
+ def assign_settings(message)
18
+ binary_settings = message[4].unpack('b4*').first.chars.map { |c| c.to_i == 1 }
19
+ self.filter = binary_settings[0] ? :c : :a
20
+ self.max_mode = binary_settings[1]
21
+ self.speed = binary_settings[2] ? :fast : :slow
22
+ self.range = message[5].to_i
23
+ end
24
+
25
+ def pack
26
+ filter_bit = filter == :c ? 1 : 0
27
+ max_mode_bit = max_mode ? 1 : 0
28
+ speed_bit = speed == :fast ? 1 : 0
29
+
30
+ boolean_options = "0#{speed_bit}#{max_mode_bit}#{filter_bit}".to_i(2)
31
+ "#{boolean_options}#{range}".to_i(16)
32
+ end
33
+
34
+ attr_accessor :filter, :max_mode, :speed, :range
35
+
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gm3156
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Maciej Ciemborowicz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hidapi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.9
27
+ description: Digital Sound Level Meter Gm1356 USB driver for Linux.
28
+ email: maciej.ciemborowicz+rubygems@gmail.com
29
+ executables:
30
+ - gm3156
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - PROTOCOL.md
35
+ - README.md
36
+ - TODO.md
37
+ - bin/gm3156
38
+ - lib/gm3156.rb
39
+ - lib/gm3156/device.rb
40
+ - lib/gm3156/errors.rb
41
+ - lib/gm3156/record.rb
42
+ - lib/gm3156/settings.rb
43
+ homepage: https://github.com/ciembor/gm1356
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.0.3
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Captures and prints data, supports editing settings.
66
+ test_files: []