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.
- checksums.yaml +7 -0
- data/PROTOCOL.md +76 -0
- data/README.md +38 -0
- data/TODO.md +7 -0
- data/bin/gm3156 +51 -0
- data/lib/gm3156.rb +7 -0
- data/lib/gm3156/device.rb +74 -0
- data/lib/gm3156/errors.rb +12 -0
- data/lib/gm3156/record.rb +29 -0
- data/lib/gm3156/settings.rb +37 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -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
|
data/PROTOCOL.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/bin/gm3156
ADDED
@@ -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
|
data/lib/gm3156.rb
ADDED
@@ -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,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: []
|