blob: 214dfcd48cde67ea2e56971c4f2f19ddbe547f3d [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"
"context"
"encoding/binary"
"errors"
"fmt"
"log"
"math"
"time"
"github.com/google/gousb"
)
type Info struct {
Serial string
}
type Config struct {
Serial *string `json:"serial"`
}
type Report struct {
Timestamp uint64
Values []float64
}
type Parameter struct {
Name string
Value 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
parameters []*Parameter
}
func (z *Zedmon) GetFieldNames() []string {
names := make([]string, len(z.fields))
for i, field := range z.fields {
names[i] = field.Name
}
return names
}
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 {
ctx, done := context.WithTimeout(context.Background(), time.Second)
defer done()
buf := make([]byte, z.inEp.Desc.MaxPacketSize)
len, err := z.readStream.ReadContext(ctx, 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() {
for {
ctx, done := context.WithTimeout(context.Background(), time.Millisecond*100)
defer done()
data := make([]byte, 2)
data[0] = 0x00
data[1] = 0x00
_, err := z.outEp.Write(data)
if err != nil {
log.Printf("Sync send error: %v", err)
continue
}
buf := make([]byte, z.inEp.Desc.MaxPacketSize)
_, err = z.inEp.ReadContext(ctx, buf)
if err == nil {
break
}
if usbErr, ok := err.(gousb.TransferStatus); ok && usbErr == gousb.TransferTimedOut {
continue
}
log.Printf("Sync read error: %v", 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", err)
return 0, 0, err
}
buf := z.read()
if buf == nil {
return 0, 0, fmt.Errorf("Can't read packet")
}
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: %v", 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
}
func parseStringFromBytes(bytesIn []byte) string {
var s string
n := bytes.IndexByte(bytesIn, 0x00)
if n != -1 {
s = string(bytesIn[:n])
} else {
s = string(bytesIn)
}
return s
}
// Returns: offset after this field, error
func (z *Zedmon) getReportFormat(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] = PACKET_TYPE_QUERY_REPORT_FORMAT
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
name := parseStringFromBytes(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 (z *Zedmon) getParameter(index uint) (bool, error) {
data := [2]byte{PACKET_TYPE_QUERY_PARAMETER_VALUE, 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 parameterValuePacket
binary.Read(bytes.NewReader(response), binary.LittleEndian, &packet)
if packet.PacketType != PACKET_TYPE_PARAMTER_VALUE {
return false, fmt.Errorf("Expected packet type %d, received %d",
PACKET_TYPE_PARAMTER_VALUE, packet.PacketType)
}
name := parseStringFromBytes(packet.Name[:])
if name == "" {
return true, nil
}
dataType := DataType(packet.DataType)
if dataType.Size() < 0 {
return false, fmt.Errorf("Can't handle Value type %d", dataType)
}
value, err := dataType.Read(bytes.NewReader(packet.Data[:]))
if err != nil {
return false, err
}
parameter := &Parameter{
Name: name,
Value: value,
}
z.parameters = append(z.parameters, parameter)
return false, nil
}
func (z *Zedmon) enumerateParameters() error {
for i := uint(0); ; i++ {
done, err := z.getParameter(i)
if err != nil {
return err
}
if done {
return nil
}
}
}
// GetResistance retreives the shunt resistance from the Zedmon firmware. If the
// shunt_resistance parameter cannot be found, or if its value is non-positive,
// the returned resistance is 0.0 and the error is non-nil.
func (z *Zedmon) GetShuntResistance() (float64, error) {
for _, parameter := range z.parameters {
if parameter.Name == "shunt_resistance" {
resistance := parameter.Value
if resistance <= 0.0 {
return 0.0, fmt.Errorf("non-positive resistance found: %g", resistance)
}
return resistance, nil
}
}
return 0.0, errors.New("resistance not found")
}
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.getReportFormat(i, &offset)
if err != nil {
return err
}
if done {
return nil
}
}
}
func Enumerate() ([]Info, error) {
ctx := gousb.NewContext()
devs, err := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool {
return desc.Vendor == 0x18d1 && desc.Product == 0xaf00
})
if err != nil {
ctx.Close()
return nil, err
}
if len(devs) == 0 {
return nil, errors.New("No zedmon device found")
}
var infos []Info
for _, d := range devs {
serial, err := d.SerialNumber()
if err != nil {
continue
}
infos = append(infos, Info{Serial: serial})
}
return infos, nil
}
func OpenZedmon(config Config) (*Zedmon, error) {
zedmon := &Zedmon{
ctx: gousb.NewContext(),
}
devs, err := zedmon.ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool {
return desc.Vendor == 0x18d1 && desc.Product == 0xaf00
})
if err != nil {
zedmon.Close()
return nil, err
}
if len(devs) == 0 {
return nil, errors.New("No zedmon device found")
}
var dev *gousb.Device
if config.Serial == nil {
dev = devs[0]
} else {
for _, d := range devs {
serial, err := d.SerialNumber()
if err != nil {
continue
}
if serial == *config.Serial {
dev = d
break
}
}
if dev == nil {
zedmon.Close()
return nil, fmt.Errorf("No zedmon device with serial %s found", *config.Serial)
}
}
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.readStream, err = zedmon.inEp.NewStream(zedmon.inEp.Desc.MaxPacketSize, 100)
if err != nil {
zedmon.Close()
return nil, err
}
zedmon.enumerateFields()
zedmon.enumerateParameters()
return zedmon, nil
}