| // 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, ×tamp) |
| 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 |
| } |