Class: Rubycam::Obsbot

Inherits:
Object
  • Object
show all
Defined in:
lib/rubycam/obsbot.rb

Overview

OBSBOT vendor commands, spoken over the camera's UVC Extension Unit (unit 2, GUID 9a1e7291-6843-4683-6d92-39bc7906ee49) via the uvcvideo driver's UVCIOC_CTRL_QUERY ioctl. Protocol reverse-engineered by the Tiny4Linux project (https://github.com/OpenFoxes/Tiny4Linux).

Constant Summary collapse

UNIT =
0x02
SELECTOR_COMMAND =
0x02
SELECTOR_STATUS =
0x06
PAYLOAD_SIZE =
60
UVC_SET_CUR =
0x01
UVC_GET_CUR =
0x81
UVCIOC_CTRL_QUERY =

struct uvc_xu_control_query { u8 unit; u8 selector; u8 query; u16 size; u8 *data; } — 16 bytes with padding on 64-bit.

Ioctl.iowr('u', 0x21, 16)
DEVICE_HINT =
'OBSBOT Tiny 2'.freeze
GROUP_SLEEP =
[0x0a, 0x02, 0xc2, 0xa0, 0x04, 0x00].freeze
WAKE_PACKET =
command02(seq: [0xa5, 0x00], checksum: [0x5f, 0xef],
group: GROUP_SLEEP,
cmd: [0xbe, 0x07, 0x00, 0x00, 0x00, 0x00])
SLEEP_PACKET =
command02(seq: [0x42, 0x00], checksum: [0xea, 0x63],
group: GROUP_SLEEP,
cmd: [0xbf, 0xfb, 0x01, 0x00, 0x00, 0x00])
GROUP_TRACKING_SPEED =
[0x0a, 0x04, 0xc4, 0x0c, 0x01, 0x00].freeze
TRACKING_SPEED_PACKETS =
{
  standard: command02(seq: [0x20, 0x00], checksum: [0xab, 0xcb],
                      group: GROUP_TRACKING_SPEED,
                      cmd: [0xe6, 0x3f, 0x00, 0x00, 0x00, 0x00]),
  sport: command02(seq: [0x21, 0x00], checksum: [0xfa, 0x0e],
                   group: GROUP_TRACKING_SPEED,
                   cmd: [0x67, 0xfe, 0x02, 0x00, 0x00, 0x00])
}.freeze
GROUP_PRESETS =
[0x0a, 0x04, 0xc4, 0x39, 0x14, 0x00].freeze
PRESET_APPENDIX =

1.0f × 4

([0x00, 0x00, 0x80, 0x3f] * 4).freeze
PRESET_PACKETS =
[
  command02(seq: [0x20, 0x00], checksum: [0x6b, 0xdc], group: GROUP_PRESETS,
            cmd: [0xd6, 0xfb, 0x00, 0x00, 0x00, 0x00], appendix: PRESET_APPENDIX),
  command02(seq: [0x1a, 0x00], checksum: [0x4b, 0x03], group: GROUP_PRESETS,
            cmd: [0xeb, 0x2a, 0x01, 0x00, 0x00, 0x00], appendix: PRESET_APPENDIX),
  command02(seq: [0x26, 0x00], checksum: [0x8b, 0xc3], group: GROUP_PRESETS,
            cmd: [0xaf, 0x19, 0x02, 0x00, 0x00, 0x00], appendix: PRESET_APPENDIX)
].freeze
GROUP_EXPOSURE_TYPE =

Switching exposure mode is two-stage: a mode-type packet on 0x02 (manual vs. automatic), then for the automatic flavours a follow-up on 0x06 choosing global or face metering.

[0x0a, 0x02, 0x82, 0x29, 0x05, 0x00].freeze
EXPOSURE_TYPE_PACKETS =
{
  manual: command02(seq: [0x16, 0x00], checksum: [0x58, 0x91],
                    group: GROUP_EXPOSURE_TYPE,
                    cmd: [0xb2, 0xaf, 0x02, 0x04, 0x00, 0x00]),
  auto: command02(seq: [0x15, 0x00], checksum: [0xa8, 0x9e],
                  group: GROUP_EXPOSURE_TYPE,
                  cmd: [0xf9, 0x27, 0x01, 0x32, 0x00, 0x00])
}.freeze
EXPOSURE_MODES =
{ manual: nil, global: [0x03, 0x01, 0x00],
face: [0x03, 0x01, 0x01] }.freeze
AI_MODES =

AI tracking modes, keyed by symbol; values are the two mode bytes as sent in the set command and reported at 0x18/0x1c of the status block.

{ no_tracking: [0, 0], normal_tracking: [2, 0],
upper_body: [2, 1], close_up: [2, 2], headless: [2, 3],
lower_body: [2, 4], desk_mode: [5, 0], whiteboard: [4, 0],
hand: [6, 0], group: [1, 0] }.freeze
AI_MODE_BY_BYTES =
AI_MODES.invert.freeze
TRACKING_SPEEDS =
%i[standard sport].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(device) ⇒ Obsbot

Returns a new instance of Obsbot.



87
88
89
90
# File 'lib/rubycam/obsbot.rb', line 87

def initialize(device)
  @device = device
  @debug = false
end

Instance Attribute Details

#debugObject

If set, sent commands and raw status reads are logged to stderr.



85
86
87
# File 'lib/rubycam/obsbot.rb', line 85

def debug
  @debug
end

Class Method Details

.command02(seq:, checksum:, group:, cmd:, appendix: [0] * 16) ⇒ Object

Command packets sent to selector 0x02:

aa 25 | seq(2) | 0c 00 | checksum(2) | group(6) | cmd(6) | appendix(16)

Sequence numbers and checksums are replayed verbatim from captures of the official software; the camera accepts them as-is.



25
26
27
28
# File 'lib/rubycam/obsbot.rb', line 25

def self.command02(seq:, checksum:, group:, cmd:, appendix: [0] * 16)
  ([0xaa, 0x25] + seq + [0x0c, 0x00] + checksum + group + cmd + appendix)
    .pack('C*')
end

Instance Method Details

#ai_mode=(mode) ⇒ Object



95
96
97
98
# File 'lib/rubycam/obsbot.rb', line 95

def ai_mode=(mode)
  m, n = AI_MODES.fetch(mode)
  send_status_command([0x16, 0x02, m, n].pack('C*'))
end

#asleep?Boolean

Returns:

  • (Boolean)


131
# File 'lib/rubycam/obsbot.rb', line 131

def asleep? = status[:asleep]

#dump(selector = SELECTOR_STATUS) ⇒ Object

Current 60-byte state of a selector as a hex string.



141
142
143
# File 'lib/rubycam/obsbot.rb', line 141

def dump(selector = SELECTOR_STATUS)
  xu_query(UVC_GET_CUR, selector).unpack1('H*')
end

#exposure_mode=(mode) ⇒ Object



114
115
116
117
118
# File 'lib/rubycam/obsbot.rb', line 114

def exposure_mode=(mode)
  metering = EXPOSURE_MODES.fetch(mode)
  send_command(EXPOSURE_TYPE_PACKETS.fetch(metering ? :auto : :manual))
  send_status_command(metering.pack('C*')) if metering
end

#goto_preset(number) ⇒ Object

Move the gimbal to a stored preset position (0..2). The camera ignores this while tracking, so callers normally switch to :no_tracking first.



106
107
108
# File 'lib/rubycam/obsbot.rb', line 106

def goto_preset(number)
  send_command(PRESET_PACKETS.fetch(number))
end

#hdr=(on) ⇒ Object



110
111
112
# File 'lib/rubycam/obsbot.rb', line 110

def hdr=(on)
  send_status_command([0x01, 0x01, on ? 1 : 0].pack('C*'))
end

#send_hex(hex, selector: SELECTOR_STATUS) ⇒ Object

---- Debug console ------------------------------------------------------- Raw access to the extension unit, mirroring Tiny4Linux's debug area.



136
137
138
# File 'lib/rubycam/obsbot.rb', line 136

def send_hex(hex, selector: SELECTOR_STATUS)
  send_to(selector, [hex.gsub(/\s/, '')].pack('H*'))
end

#sleep!Object



93
# File 'lib/rubycam/obsbot.rb', line 93

def sleep! = send_command(SLEEP_PACKET)

#statusObject

NOTE: on newer Tiny 2 firmware the status block can lag behind mode changes by several seconds; treat it as eventually consistent.



122
123
124
125
126
127
128
129
# File 'lib/rubycam/obsbot.rb', line 122

def status
  raw_status.then do |bytes|
    { asleep: bytes[0x02] == 1,
      hdr: bytes[0x06] != 0,
      ai_mode: AI_MODE_BY_BYTES.fetch([bytes[0x18], bytes[0x1c]], :unknown),
      tracking_speed: decode_speed(bytes) }
  end
end

#tracking_speed=(speed) ⇒ Object



100
101
102
# File 'lib/rubycam/obsbot.rb', line 100

def tracking_speed=(speed)
  send_command(TRACKING_SPEED_PACKETS.fetch(speed))
end

#wake!Object



92
# File 'lib/rubycam/obsbot.rb', line 92

def wake! = send_command(WAKE_PACKET)