blob: 25e14e0f5177cb826111f42695bd3466a55d39b8 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package lib
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"math"
"time"
"github.com/google/gousb"
)
type Report struct {
Timestamp uint64
Values []float64
}
type Zedmon struct {
ctx *gousb.Context
dev *gousb.Device
config *gousb.Config
intf *gousb.Interface
inEpNum int
outEpNum int
inEp *gousb.InEndpoint
outEp *gousb.OutEndpoint
readStream *gousb.ReadStream
reportingEnabled bool
fields []*Field
}
func (z *Zedmon) Close() {
if z.reportingEnabled {
z.DisableReporting()
}
if z.readStream != nil {
z.readStream.Close()
}
if z.intf != nil {
z.intf.Close()
}
if z.config != nil {
z.config.Close()
}
if z.dev != nil {
z.dev.Close()
}
if z.ctx != nil {
z.ctx.Close()
}
}
func (z *Zedmon) read() []byte {
buf := make([]byte, z.inEp.Desc.MaxPacketSize)
len, err := z.readStream.Read(buf)
if err != nil {
log.Printf("Read stream error: %v", err)
return nil
}
return buf[:len]
}
// Synchronize with the device.
//
// It is possible for out endpoint data toggles to get out of phase on on
// successive invocations. Repeating a get format report command until we
// get a response synchronizes them.
func (z *Zedmon) sync() {
z.inEp.Timeout = time.Millisecond * 100
for {
data := make([]byte, 2)
data[0] = 0x00
data[1] = 0x00
_, err := z.outEp.Write(data)
if err != nil {
log.Printf("Sync Send Error: %v\n", err)
continue
}
buf := make([]byte, z.inEp.Desc.MaxPacketSize)
_, err = z.inEp.Read(buf)
if err == nil {
break
}
if usbErr, ok := err.(gousb.TransferStatus); ok && usbErr == gousb.TransferTimedOut {
continue
}
log.Printf("Sync Read Error: %v\n", err)
}
}
func (z *Zedmon) GetTimeOffset() (time.Duration, time.Duration, error) {
bestRoundTripDuration := time.Duration(math.MaxInt64)
bestOffset := time.Duration(0)
for i := 0; i < 10; i++ {
data := make([]byte, 1)
data[0] = 0x01
start := time.Now()
_, err := z.outEp.Write(data)
if err != nil {
log.Printf("Sync Send Error: %v\n", err)
return 0, 0, err
}
buf := z.read()
if buf == nil {
return 0, 0, fmt.Errorf("Can't read packet\n")
}
end := time.Now()
reader := bytes.NewReader(buf[1:])
var rawTimestamp uint64
err = binary.Read(reader, binary.LittleEndian, &rawTimestamp)
timestamp := time.Unix(int64(rawTimestamp/1000000), int64((rawTimestamp%1000000)*1000))
delta := end.Sub(start)
median := start.Add(delta / 2)
offset := median.Sub(timestamp)
if delta < bestRoundTripDuration {
bestRoundTripDuration = delta
bestOffset = offset
}
}
return bestOffset, bestRoundTripDuration, nil
}
func (z *Zedmon) EnableReporting() error {
data := make([]byte, 1)
data[0] = PACKET_TYPE_ENABLE_REPORTING
_, err := z.outEp.Write(data)
if err != nil {
return err
}
z.reportingEnabled = true
return nil
}
func (z *Zedmon) DisableReporting() error {
data := make([]byte, 1)
data[0] = PACKET_TYPE_DISABLE_REPORTING
_, err := z.outEp.Write(data)
if err != nil {
return err
}
// Give time for the packet to be sent. This could be avoided by adding
// ACKs to the protocol.
time.Sleep(time.Millisecond * 100)
z.reportingEnabled = false
return nil
}
func (z *Zedmon) SetOuput(index uint8, value bool) error {
packet := setOutputPacket{
PacketType: PACKET_TYPE_SET_OUTPUT,
OutputIndex: index,
OutputValue: value,
}
buf := new(bytes.Buffer)
err := binary.Write(buf, binary.LittleEndian, &packet)
if err != nil {
return fmt.Errorf("SetOuput: binary.Write failed:", err)
}
_, err = z.outEp.Write(buf.Bytes())
if err != nil {
return err
}
// Give time for the packet to be sent. This could be avoided by adding
// ACKs to the protocol.
time.Sleep(time.Millisecond * 100)
return nil
}
// TODO: Add mechanism for pre-allocating reports.
func (z *Zedmon) ReadReports() ([]*Report, error) {
buf := z.read()
if buf[0] != PACKET_TYPE_REPORT {
return nil, fmt.Errorf("Unexpected packet type: %02x", buf[0])
}
var reports []*Report
reader := bytes.NewReader(buf[1:])
for reader.Len() > 0 {
var timestamp uint64
err := binary.Read(reader, binary.LittleEndian, &timestamp)
if err != nil {
return nil, err
}
report := &Report{
Timestamp: timestamp,
Values: make([]float64, 0, len(z.fields)),
}
for _, field := range z.fields {
val, err := field.Decode(reader)
if err != nil {
return nil, err
}
report.Values = append(report.Values, val)
}
reports = append(reports, report)
}
return reports, nil
}
// Returns: offset after this field, error
func (z *Zedmon) getFormatReport(index uint, offset *int) (bool, error) {
if index >= 0xff {
return false, fmt.Errorf("Index %d out of range.", index)
}
data := make([]byte, 2)
data[0] = 0x00
data[1] = uint8(index)
numBytes, err := z.outEp.Write(data)
if err != nil {
return false, err
}
if numBytes != len(data) {
return false, fmt.Errorf("Incomplete write %d != %d", numBytes, len(data))
}
response := z.read() //<-z.readChan
var packet reportFormatPacket
binary.Read(bytes.NewReader(response), binary.LittleEndian, &packet)
if packet.ValueIndex == 0xff {
return true, nil
}
dataType := DataType(packet.ValueType)
if dataType.Size() < 0 {
return false, fmt.Errorf("Can't handle Value type %d.", dataType)
}
// TODO: validate Unit
var name string
n := bytes.IndexByte(packet.Name[:], 0x00)
if n != -1 {
name = string(packet.Name[:n])
} else {
name = string(packet.Name[:])
}
field := &Field{
Offset: *offset,
Name: name,
Type: dataType,
Unit: Unit(packet.ValueUnit),
Scale: packet.Scale,
}
z.fields = append(z.fields, field)
*offset += dataType.Size()
return false, nil
}
func findZedmonInterface(dev *gousb.Device) (config int, intf int, setting int, err error) {
for cfgNum, cfgDesc := range dev.Desc.Configs {
for intfNum, intfDesc := range cfgDesc.Interfaces {
for settingNum, settingDesc := range intfDesc.AltSettings {
if settingDesc.Class == gousb.ClassVendorSpec &&
settingDesc.SubClass == gousb.ClassVendorSpec &&
settingDesc.Protocol == 0x0 &&
len(settingDesc.Endpoints) == 2 {
return cfgNum, intfNum, settingNum, nil
}
}
}
}
return 0, 0, 0, fmt.Errorf("Can't find zedmon interface.")
}
func (z *Zedmon) enumerateFields() error {
offset := 0
for i := uint(0); ; i++ {
done, err := z.getFormatReport(i, &offset)
if err != nil {
return err
}
if done {
return nil
}
}
}
func OpenZedmon() (*Zedmon, error) {
zedmon := &Zedmon{
ctx: gousb.NewContext(),
}
dev, err := zedmon.ctx.OpenDeviceWithVIDPID(0x18d1, 0xaf00)
if err != nil {
zedmon.Close()
return nil, err
}
configNum, intfNum, settingNum, err := findZedmonInterface(dev)
if err != nil {
zedmon.Close()
return nil, err
}
zedmon.config, err = dev.Config(configNum)
if err != nil {
zedmon.Close()
return nil, err
}
zedmon.intf, err = zedmon.config.Interface(intfNum, settingNum)
if err != nil {
zedmon.Close()
return nil, err
}
// TODO: We should verify this will succeed in findZedmonInterface().
for _, epDesc := range zedmon.intf.Setting.Endpoints {
switch epDesc.Direction {
case gousb.EndpointDirectionIn:
zedmon.inEpNum = epDesc.Number
case gousb.EndpointDirectionOut:
zedmon.outEpNum = epDesc.Number
}
}
zedmon.inEp, err = zedmon.intf.InEndpoint(zedmon.inEpNum)
if err != nil {
zedmon.Close()
return nil, err
}
zedmon.outEp, err = zedmon.intf.OutEndpoint(zedmon.outEpNum)
if err != nil {
zedmon.Close()
return nil, err
}
zedmon.sync()
zedmon.inEp.Timeout = time.Second * 1
zedmon.readStream, err = zedmon.inEp.NewStream(zedmon.inEp.Desc.MaxPacketSize, 100)
if err != nil {
zedmon.Close()
return nil, err
}
zedmon.enumerateFields()
return zedmon, nil
}