luma

package module
v0.2.0 Latest Latest
Warning

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

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

README

go-viture/luma

Talk to a VITURE Luma Ultra headset from pure Go, CGO_ENABLED=0, with no vendor library.

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

v, err := g.Version()      // 02 01 09 on the headset this was written for

What is proven

Measured on real hardware, 2026-09-06:

write → endpoint 0x04        read ← endpoint 0x85        device 35ca:1104

fa 55 80 00 00   →   fa 55 80 00 00 02 01 09
fa 55 c3 00 00   →   fa 55 c3 01 00 00

The envelope:

FA 55 | command | length (16-bit big-endian) | payload

The headset echoes the command byte, so replies match requests without a sequence number — and that is worth checking, because the inbound pipe is shared and a late answer to an earlier question has the right shape and arrives at the right moment.

Drain before every command. Not housekeeping: the vendor's own code clears the inbound pipe before each one, for exactly that reason. Ask does it.

⛔ The headset never speaks first

Measured over 75 seconds, with the buttons on the arm pressed and the head moving, listening on all five inbound pipes: twelve identical frames in the first ten milliseconds — a buffer flushed at open — and then nothing at all.

It is a request-and-response device. No amount of listening decodes it. That is the opposite of the VITURE Beast, which announces what its own buttons do; the two do not even share an envelope, so do not carry habits across.

⚠ What is NOT settled

The length field of a reply.

0x80 → "00 00" then THREE bytes
0xc3 → "01 00" then ONE byte

Read little-endian the second works and the first does not. Reply therefore hands back those two bytes and everything after them, unparsed, rather than pretending. Guessing here is how nineteen attempts were lost on the sibling headset, which answers in one byte order and accepts commands in another.

⛔ Two commands, and twenty-five deliberately absent

0x80 and 0xc3 are the frames VITURE's own carina_a1088_get_firmware_version sends — which is why they were safe to try first: a version read changes nothing, repeats safely, and its success is visible.

Twenty-five other command bytes are known to exist. None is here. Knowing a number is not knowing what it does, and an unknown opcode written to a headset somebody is wearing is not a probe.

Requirements

  • macOS for the transport; the frame model builds and is tested everywhere.
  • VITURE's SpaceWalker must be closed. It holds the vendor interface while it runs, and Open then fails with the system's exclusive-access error.

Provenance

The envelope and the endpoints were read out of libcarina_vio.dylib, the vendor's own library, shipped inside SpaceWalker.app. Interoperability is a recognised purpose for that (EU Directive 2009/24/EC, Article 6). Nothing was copied: this is an independent implementation of an interface.

Licence

BSD-3-Clause.

Documentation

Overview

Package luma talks to a VITURE Luma Ultra headset in pure Go, with CGO_ENABLED=0.

⛔ THE HEADSET NEVER SPEAKS FIRST. Measured 2026-09-06 over 75 seconds, with the buttons on the arm pressed and the head moving, listening on all five of its inbound pipes: twelve identical frames in the first ten milliseconds -- a buffer flushed at open -- and then nothing at all. It is a request and response device, so no amount of listening decodes it. Every fact here came from asking.

That is the opposite of the Beast, which ANNOUNCES what its own buttons do and can be decoded by watching. Do not carry habits from one to the other: they do not even share an envelope.

Index

Constants

View Source
const (
	// VendorID and ProductID are the Luma Ultra's raw vendor interface. The
	// companion 35ca:1102 is NOT this: it is an audio-key set whose name --
	// "VITURE Microphone" -- has now misled two separate investigations.
	VendorID  uint16 = 0x35ca
	ProductID uint16 = 0x1104

	// EndpointOut and EndpointIn are where a command goes and where its answer
	// comes back.
	EndpointOut byte = 0x04
	EndpointIn  byte = 0x85
)

The device, and the two pipes that carry the protocol.

⭐ MEASURED, not assumed. 35ca:1104 publishes seven endpoints; the two below are the ones the vendor's own getters use, and the ones this package proved on the hardware.

View Source
const (
	MsgAppFirmwareVersion uint16 = 0x0001
	MsgDPVersion          uint16 = 0x000b
	MsgBoardSerial        uint16 = 0x0010
	MsgPackageSerial      uint16 = 0x0011
	MsgOSDVersion         uint16 = 0x0063
)

Message ids on the data envelope. Every one of these is a READ.

View Source
const (
	DataEndpointOut byte = 0x06
	DataEndpointIn  byte = 0x87
)

Data endpoints, as measured. See the comment above for why they are not the MCU pair.

View Source
const (
	// DrainTimeout bounds each read of the leftover-clearing pass.
	DrainTimeout = 100 * time.Millisecond
	// Timeout bounds the command and its answer.
	Timeout = 1500 * time.Millisecond
	// DrainReads is how many leftover frames are cleared before asking.
	DrainReads = 4
)

How long each half of an exchange is given.

⭐ THE VENDOR'S OWN NUMBERS: 100 ms for the drain and 1500 ms for the command and its answer, read out of the getters this package was decoded from.

View Source
const (
	// CmdVersionA answered "02 01 09" on the headset this was written for.
	CmdVersionA byte = 0x80
	// CmdVersionB answered "00", and is the second frame the vendor's getter
	// sends. What distinguishes the two is not established.
	CmdVersionB byte = 0xc3
)

Commands this package has PROVEN on the hardware.

⛔ NOTHING HERE IS GUESSED. Both are frames the vendor's own carina_a1088_get_firmware_version sends, which is why they were safe to try: a version read changes nothing. Twenty-five other command bytes are known to exist and are deliberately absent, because knowing a number is not knowing what it does -- and an unknown opcode written to a headset somebody is wearing is not a probe.

Variables

View Source
var ErrNoDevice = errors.New("luma: no VITURE Luma Ultra found")

ErrNoDevice says no Luma Ultra is on the bus.

View Source
var ErrNotAFrame = errors.New("luma: not a frame")

ErrNotAFrame says a reply did not begin with Magic.

View Source
var ErrRefused = errors.New("luma: the headset refused the request")

ErrRefused says the headset answered with a non-zero status.

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

ErrUnsupported is returned off macOS.

View Source
var ErrWrongCommand = errors.New("luma: the reply answers a different command")

ErrWrongCommand says a reply answered a different command than the one asked.

⭐ THE HEADSET REPEATS THE COMMAND BYTE, which is what makes this checkable without a sequence number -- and worth checking, because the pipe is shared and a late answer to an earlier question looks exactly like an early answer to this one.

View Source
var Magic = [2]byte{0xFA, 0x55}

Magic is the two bytes every frame begins with, in the order they go on the wire.

Functions

func DataRequest added in v0.2.0

func DataRequest(msg uint16, payload []byte) []byte

DataRequest builds one request on the data envelope, padded to the packet size the device expects.

func ParseData added in v0.2.0

func ParseData(b []byte, want uint16) (string, error)

ParseData reads a reply on the data envelope.

⛔ IT TRIMS TRAILING NULs AND NOTHING ELSE. The serial fields are fixed-width and come NUL-padded, so a caller that kept them would put invisible bytes in a settings window; a caller that trimmed spaces too would be guessing.

func Request

func Request(command byte, payload []byte) []byte

Request builds the frame that asks for a command.

FA 55 | command | length | payload

⚠ THE LENGTH IS BIG-ENDIAN HERE, AND ONLY HERE. The vendor's frame builders store it with a 16-bit little-endian write of 0x0100, 0x0200, 0x0300, 0x0800 and 0x4000 -- which put the bytes 00 01, 00 02, 00 03, 00 08, 00 40 on the wire, and the number of payload bytes that follow matches every time when they are read big-endian. A REPLY appears to use the other order, and that is not yet settled: see Reply.

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 headset and claims its vendor interface.

⛔ THE INTERFACE IS EXCLUSIVE. VITURE's own SpaceWalker holds it while it runs, and Open then fails with the system's exclusive-access error rather than silently reading nothing. Measured: it opens freely with that app closed and is refused with it running.

func (*Glasses) Ask

func (g *Glasses) Ask(command byte, payload []byte) (Reply, error)

Ask sends one command and returns what came back.

⛔ IT DRAINS FIRST, and that is not housekeeping. The vendor's code clears the inbound pipe before EVERY command, because the pipe is shared and a late answer to an earlier question is indistinguishable from an early answer to this one -- it has the right shape, it arrives at the right moment, and it says the wrong thing. The echoed command byte catches what the drain misses.

func (*Glasses) AskData added in v0.2.0

func (g *Glasses) AskData(msg uint16) (string, error)

AskData sends one data-envelope request and returns the text it answered.

func (*Glasses) ChipVersion added in v0.2.0

func (g *Glasses) ChipVersion() ([]byte, error)

ChipVersion is the version of the CHIP's firmware, and not the product's.

⛔⛔ IT WAS CALLED Version, AND THAT WAS WRONG. It answers "02 01 09" on a headset whose own updater reads 0.01.101_20260605, and an earlier note took those three bytes for "firmware 2.1.9". The vendor's library settles it: carina_a1088_get_firmware_version SENDS NOTHING -- it hands back a cached string filled by a routine that picks between two getters on a model field, and the getter this headset reaches NAMES ITSELF in its own log strings, "getchipfwver" and "get chip fw ver data status". So 0x80 is a correct read of the chip's firmware; only the label was false.

⚠ AND THE BYTES ARE STILL JUST BYTES. Nothing confirms that "02 01 09" is to be shown as 2.1.9. A caller that wants to print it can; this will not pretend on its behalf.

For the version a person recognises, and the serial the vendor prints on its own screen, see Glasses.Info.

func (*Glasses) Close

func (g *Glasses) Close() error

Close releases it.

func (*Glasses) Info added in v0.2.0

func (g *Glasses) Info() (Info, error)

Info asks the headset what it is: the firmware a person would recognise, and its two serial numbers.

⛔ IT DOES NOT FALL BACK. A version this could not read is left empty rather than filled with the chip version, which is a different number entirely and would be wrong on screen in a way nobody could catch.

type Info added in v0.2.0

type Info struct {
	// FirmwareVersion is the application firmware, VERBATIM. The vendor's
	// updater shows it without its leading component: "12.0.01.101_20260605"
	// here is "0.01.101_20260605" there.
	FirmwareVersion string
	// BoardSerial identifies the board; PackageSerial identifies the product,
	// and is the one the vendor's updater labels "SN".
	BoardSerial, PackageSerial string
}

Info is what the headset says about itself.

type Reply

type Reply struct {
	// Command is the byte the headset echoed back.
	Command byte
	// Field is bytes 3 and 4, whatever they mean.
	Field [2]byte
	// Rest is everything after them, unparsed.
	Rest []byte
}

Reply is what came back, split as far as it is understood.

⚠ THE LENGTH FIELD OF A REPLY IS NOT SETTLED, so this hands back the two bytes AND everything after them rather than pretending to know. Measured:

fa 55 80 00 00 02 01 09     "00 00" then THREE bytes
fa 55 c3 01 00 00           "01 00" then ONE byte

Read little-endian the second is right and the first is not. Two readings remain open -- those bytes may not be a length at all in a reply -- and guessing here is how nineteen attempts were lost on the sibling headset, which turned out to answer in one byte order and accept commands in another.

func ParseReply

func ParseReply(b []byte, want byte) (Reply, error)

ParseReply splits a frame the headset sent.

func (Reply) String

func (r Reply) String() string

String renders a reply the way a probe prints it.

Jump to

Keyboard shortcuts

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