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 ¶
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.
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.
const ( DataEndpointOut byte = 0x06 DataEndpointIn byte = 0x87 )
Data endpoints, as measured. See the comment above for why they are not the MCU pair.
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.
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 ¶
var ErrNoDevice = errors.New("luma: no VITURE Luma Ultra found")
ErrNoDevice says no Luma Ultra is on the bus.
var ErrNotAFrame = errors.New("luma: not a frame")
ErrNotAFrame says a reply did not begin with Magic.
var ErrRefused = errors.New("luma: the headset refused the request")
ErrRefused says the headset answered with a non-zero status.
var ErrUnsupported = errors.New("luma: only macOS is wired up")
ErrUnsupported is returned off macOS.
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.
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
DataRequest builds one request on the data envelope, padded to the packet size the device expects.
func ParseData ¶ added in v0.2.0
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 ¶
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 ¶
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 ¶
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
AskData sends one data-envelope request and returns the text it answered.
func (*Glasses) ChipVersion ¶ added in v0.2.0
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) Info ¶ added in v0.2.0
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 ¶
ParseReply splits a frame the headset sent.