beast

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-viture/beast

Talk to a VITURE Beast headset from pure Go, CGO_ENABLED=0.

g, err := beast.Open()
if err != nil { return err }
defer g.Close()

mode, err := g.Get(viture.MsgNativeDisplayMode)   // 0x31 = 1920x1080 @60Hz
err = g.Set(viture.MsgDisplayMode, viture.Mode3840x1080At60)  // side by side
at, err := g.Nudge(0x22, +1, 8)                   // one step brighter

⛔ Not the same protocol as the Luma

A Beast frame begins 0x10; a Luma frame begins FA 55. They do not share an envelope, a transport, or a habit. What they share is a house style: ask before you write, and judge by what the device answers.

⭐ The headset answers every command with a status

0  taken        4  refused        6  too short

That is worth more than watching the display: a screen that does not change cannot tell a refusal from a command that never arrived. Finding that out took nineteen failed writes, all judged by a screen.

⭐ And a READ answering 2 means "there is no such message" — which makes the whole id space discoverable without ever writing. Twenty messages exist between 0x00 and 0x7f; every other id answers 2.

⛔ The value goes in twice, little-endian

10 00 24 01 02 00 32 00 32 00        ← side by side
                ^^^^^ ^^^^^

That one detail is what nineteen attempts had wrong. The headset's own replies carry a value little-endian and then big-endian, and copying that shape into a command has it refused. A reply and a command are not the same frame.

⚠ A setting can be there and then not be

Measured: 0x22 (brightness) and 0x30 (volume) both answered a read one evening and were both gone ninety minutes later — same cable, no replug, every other id still answering. 0x22 came back the next morning and was gone again by lunchtime.

So ErrNoSetting is a state this reports, never a fact compiled in.

Where the frames come from

go-macos/iokit/viture, which decoded them — not copied here. This package is the transport and the ergonomics: open the right interface, listen before asking, match the reply on message and direction, and hand back what the headset said.

Listen before asking: the headset answers in milliseconds, and a listener started afterwards misses its own answer.

Requirements

macOS for the transport; the frame and exchange logic builds and is tested everywhere.

Licence

BSD-3-Clause.

Documentation

Overview

Package beast talks to a VITURE Beast headset in pure Go, with CGO_ENABLED=0.

⛔ IT IS NOT THE SAME PROTOCOL AS THE LUMA, and the two do not even share an envelope: a Beast frame begins 0x10 and a Luma frame begins FA 55. Habits do not carry across. What they do share is a house style -- ask before you write, and judge by what the device answers rather than by what the screen does.

⭐ THE HEADSET ANSWERS EVERY COMMAND WITH A STATUS, and that is worth more than watching the display: a screen that does not change cannot tell a refusal from a command that never arrived. Finding that out took nineteen failed writes.

The frames themselves live in go-macos/iokit/viture, which decoded them. This package is the transport and the ergonomics on top: open the right interface, listen before asking, and hand back what the headset said.

Index

Constants

View Source
const (
	VendorID  uint16 = 0x35ca
	UsagePage uint16 = 0xff00
)

The device this speaks to.

⭐ 35ca:1201 ON THE VENDOR USAGE PAGE is the control interface, and it is the only one of the three the glasses publish that carries the protocol: the others are called "VITURE Microphone" and are a Consumer-page audio set. The name misleads and the usage page does not -- a mistake this fleet has now made in both directions.

View Source
const Wait = 2 * time.Second

Wait is how long the headset is given to answer.

Generous for a device that answers in milliseconds, and short enough that a menu row does not appear to hang.

Variables

View Source
var ErrNoAnswer = errors.New("beast: the headset did not answer")

ErrNoAnswer says the headset did not reply in time.

View Source
var ErrNoDevice = errors.New("beast: no VITURE Beast found")

ErrNoDevice says no Beast control interface is on the bus.

View Source
var ErrNoSetting = errors.New("beast: the headset has no such setting")

ErrNoSetting means the headset answered that it has no such message.

⚠ AND IT IS A STATE, NOT A FACT ABOUT THE PROTOCOL. Measured 2026-09-05: 0x22 and 0x30 both answered a read at 17:00 and both were gone by 18:46, with the same cable and no replug -- while every other id still answered. 0x22 came back the next morning. So report it; do not compile it in.

View Source
var ErrUnsupported = errors.New("beast: only macOS is wired up")

ErrUnsupported is returned off macOS.

Functions

This section is empty.

Types

type Glasses

type Glasses struct {
	// contains filtered or unexported fields
}

Glasses is one open headset.

func Open

func Open() (*Glasses, error)

Open finds the Beast's control interface and claims it.

func (*Glasses) Close

func (g *Glasses) Close() error

Close releases it.

⛔ IT WAITS FOR THE LISTENER. Closing a HID device while a stream still holds it does not fail, it ends the PROCESS -- macOS kills the program with "os_unfair_lock is corrupt", no Go panic, no stack, nothing on the program's own output. go-macos/iokit refuses that now; this does it in the right order anyway.

func (*Glasses) Get

func (g *Glasses) Get(msg byte) (uint16, error)

Get reads one setting and returns its value.

⛔ A READ CHANGES NOTHING, which is what makes it the right first experiment on any device: it is safe to repeat, and its success is visible in a way a command's is not.

⭐ AND A READ ANSWERING 2 MEANS "NO SUCH MESSAGE", which makes the whole id space discoverable without ever writing. Measured: twenty messages exist between 0x00 and 0x7f and every other id answers 2.

func (*Glasses) Nudge

func (g *Glasses) Nudge(msg byte, by int, max uint16) (uint16, error)

Nudge moves a setting by one step, clamped, and says where it ended up.

⛔ IT READS FIRST. A key that means "brighter" has to know what it is brighter than, and the headset is the only thing that knows: somebody may have used the buttons on the arm since anything here last looked.

func (*Glasses) Set

func (g *Glasses) Set(msg byte, value uint16) error

Set writes one setting and reports what the headset said about it.

⛔ THE VALUE GOES IN TWICE, LITTLE-ENDIAN. That one detail is what nineteen attempts had wrong: the headset's own REPLIES carry a value little-endian and then big-endian, and copying that shape into a command has it refused. A reply and a command are not the same frame.

type Status

type Status uint16

Status is what the headset answers to a write.

⭐ IT ANSWERS EVERY ONE, and that is the whole reason this package judges by the reply rather than by the display. Nineteen writes were lost watching a screen: a screen that does not change cannot tell a refusal from a command that never arrived, and one read settled in a second what those nineteen had not.

const (
	// StatusOK means the command was taken.
	StatusOK Status = 0
	// StatusRefused means the headset understood and said no -- which is how
	// its own limits were mapped: brightness accepted 0 to 8 and refused 9,
	// with nothing to watch and nobody guessing.
	StatusRefused Status = 4
	// StatusTooShort means the command carried fewer than two payload bytes.
	StatusTooShort Status = 6
)

What the headset says back.

func (Status) Err

func (s Status) Err() error

Err turns a status into an error, or nil.

func (Status) String

func (s Status) String() string

String renders a status the way an error message should read.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL