package crm3000 import ( "bytes" "encoding/binary" "errors" "fmt" "io" "reflect" "strconv" "time" "src.dualinventive.com/go/dinet/ditime" "src.dualinventive.com/go/dinet/rpc" ) type subTypeID byte const ( subTypeIDBatteryVoltage subTypeID = 0x00 subTypeIDPT1000Temperature subTypeID = 0x01 subTypeIDCPUTemperature subTypeID = 0x02 subTypeIDAccelero subTypeID = 0x03 subTypeIDCapTouch subTypeID = 0x04 subTypeIDModemStatsBinary subTypeID = 0x06 subTypeIDTimestampSeconds subTypeID = 0x0b subTypeIDTimeDeltaSeconds subTypeID = 0x0c subTypeIDAlert subTypeID = 0x0d subTypeIDSerialAndFwVersion subTypeID = 0x0e subTypeIDIMEI subTypeID = 0x13 ) // Message represents a single CRM3000 decoded message type Message struct { BatteryVoltage []float32 BatteryState []rpc.BatteryState PT1000Temperature []float32 CPUTemperature []float32 Accelero []AcceleroValue CapTouch []uint16 ModemStats *ModemStats Timestamp time.Time Timedelta time.Duration SerialAndFwVersion *SerialAndFwVersion IMEI string } // AcceleroValue holds a single accelero sensor value for all three axises type AcceleroValue struct { X uint8 `json:"x" msgpack:"x"` Y uint8 `json:"y" msgpack:"y"` Z uint8 `json:"z" msgpack:"z"` } // ModemStats holds a single modem statistics // nolint: maligned type ModemStats struct { SignalPower uint16 TotalPower uint16 TXPower uint16 TXTime uint32 RXTime uint32 CelltowerID uint32 ECL uint8 SNR uint16 EARFCN uint16 PCI uint16 RSRQ uint16 } // SerialAndFwVersion is the device information which includes the product, serial numbers and firmware version type SerialAndFwVersion struct { ProductNumber uint16 SerialNumber uint32 FwVersionMajor uint8 FwVersionMinor uint8 FwVersionRev uint8 FwVersionBeta uint8 } // messagePrefix is the CRM3000 message prefix (header) const messagePrefix = "\x01\x00\x01\x00\x01" var ( // ErrUnsupportedSubTypeAlert when the message contains an alert ErrUnsupportedSubTypeAlert = errors.New("crm3000: alert subtype not supported") // ErrInvalidPrefix when the message prefix is incorrect ErrInvalidPrefix = errors.New("crm3000: invalid message prefix") ) // sampleIntervals is the LUT of the spacing between the samples // nolint: gochecknoglobals var sampleIntervals map[subTypeID]time.Duration // ErrUnsupportedSubTypeID when the subTypeID is unsupported func ErrUnsupportedSubTypeID(pid byte) error { return fmt.Errorf("crm3000: message has unsupported payload id: pid %#v", pid) } // nolint: gochecknoinits func init() { sampleIntervals = map[subTypeID]time.Duration{ subTypeIDBatteryVoltage: 300 * time.Second, subTypeIDCPUTemperature: 300 * time.Second, subTypeIDAccelero: 300 * time.Second, subTypeIDPT1000Temperature: 300 * time.Second, subTypeIDCapTouch: 1800 * time.Second, } } // Decode data into a Message func Decode(data []byte) (*Message, error) { if !bytes.HasPrefix(data, []byte(messagePrefix)) { return nil, ErrInvalidPrefix } msg := &Message{} var err error r := bytes.NewReader(data[len(messagePrefix):]) for { err = msg.decode(r) if err != nil { if err == io.EOF { err = nil } break } } return msg, err } // ResultValueItems converts the samples based on timestamp to rpc.ResultValueItem func (msg *Message) ResultValueItems(ts time.Time) []*rpc.ResultValueItem { var values []*rpc.ResultValueItem values = append(values, msg.batteryVoltageResultValueItems(ts)...) values = append(values, msg.batteryStateResultValueItems(ts)...) values = append(values, msg.pt1000TemperatureResultValueItems(ts)...) values = append(values, msg.cpuTemperatureResultValueItems(ts)...) values = append(values, msg.acceleroResultValueItems(ts)...) values = append(values, msg.capTouchResultValueItems(ts)...) return values } // nolint: gocyclo func (msg *Message) decode(r *bytes.Reader) error { // subTypeID == subType pid, err := r.ReadByte() if err != nil { return err } switch subTypeID(pid) { case subTypeIDBatteryVoltage: err = msg.readBatteryVoltage(r) case subTypeIDPT1000Temperature: err = msg.readPT1000Temperature(r) case subTypeIDCPUTemperature: err = msg.readCPUTemperature(r) case subTypeIDCapTouch: err = msg.readCapTouch(r) case subTypeIDAccelero: err = msg.readAccelero(r) case subTypeIDModemStatsBinary: err = msg.readModemStatsBinary(r) case subTypeIDTimestampSeconds: err = msg.readTimestampSeconds(r) case subTypeIDTimeDeltaSeconds: err = msg.readTimeDeltaSeconds(r) case subTypeIDAlert: // Currently we don't support Alert parsing as we have no way to configure them in the first place err = ErrUnsupportedSubTypeAlert case subTypeIDSerialAndFwVersion: err = msg.readSerialAndFwVersion(r) case subTypeIDIMEI: err = msg.readIMEI(r) default: return ErrUnsupportedSubTypeID(pid) } return err } func convBatteryVoltageToState(volts float32) rpc.BatteryState { return rpc.BatteryStateFull } func (msg *Message) readBatteryVoltage(r io.Reader) error { var mV uint16 err := binary.Read(r, binary.BigEndian, &mV) if err != nil { return err } volts := float32(mV) / 1000 msg.BatteryVoltage = append(msg.BatteryVoltage, volts) msg.BatteryState = append(msg.BatteryState, convBatteryVoltageToState(volts)) return nil } func (msg *Message) readPT1000Temperature(r io.Reader) error { var mC uint32 err := binary.Read(r, binary.BigEndian, &mC) if err != nil { return err } msg.PT1000Temperature = append(msg.PT1000Temperature, float32(mC)/1000) return nil } func (msg *Message) readCPUTemperature(r io.Reader) error { var mC uint32 err := binary.Read(r, binary.BigEndian, &mC) if err != nil { return err } msg.CPUTemperature = append(msg.CPUTemperature, float32(mC)/1000) return nil } func (msg *Message) readCapTouch(r io.Reader) error { var capVal uint16 err := binary.Read(r, binary.BigEndian, &capVal) if err != nil { return err } msg.CapTouch = append(msg.CapTouch, capVal) return nil } func (msg *Message) readAccelero(r io.Reader) error { var accelVal AcceleroValue err := binary.Read(r, binary.BigEndian, &accelVal) if err != nil { return err } msg.Accelero = append(msg.Accelero, accelVal) return nil } func (msg *Message) readModemStatsBinary(r io.Reader) error { var modemStats ModemStats err := binary.Read(r, binary.BigEndian, &modemStats) if err != nil { return err } msg.ModemStats = &modemStats return nil } func (msg *Message) readTimestampSeconds(r io.Reader) error { var v uint32 err := binary.Read(r, binary.BigEndian, v) if err != nil { return err } msg.Timestamp = time.Unix(int64(v), 0) return nil } func (msg *Message) readTimeDeltaSeconds(r io.Reader) error { var v uint16 err := binary.Read(r, binary.BigEndian, v) if err != nil { return err } msg.Timedelta = time.Duration(v) * time.Second return nil } func (msg *Message) readSerialAndFwVersion(r io.Reader) error { var serialAndFwVersion SerialAndFwVersion err := binary.Read(r, binary.BigEndian, &serialAndFwVersion) if err != nil { return err } msg.SerialAndFwVersion = &serialAndFwVersion return nil } func (msg *Message) readIMEI(r io.Reader) error { var v uint64 err := binary.Read(r, binary.BigEndian, &v) if err != nil { return err } imei := strconv.FormatUint(v, 10) msg.IMEI = imei return nil } func convResultValueItems(uid uint16, ts time.Time, interval time.Duration, values interface{}) []*rpc.ResultValueItem { var rvalues []*rpc.ResultValueItem //gocritic: prefer to keep this future proof for when cases will be added. //nolint:gocritic switch reflect.TypeOf(values).Kind() { case reflect.Slice: slice := reflect.ValueOf(values) sliceLen := slice.Len() timestamp := ts.Add(-interval * (time.Duration(sliceLen - 1))) for i := 0; i < sliceLen; i++ { rvalue := &rpc.ResultValueItem{ UID: uid, Time: ditime.FromTime(timestamp), Value: slice.Index(i).Interface(), } rvalues = append(rvalues, rvalue) timestamp = timestamp.Add(interval) } } return rvalues } func (msg *Message) batteryVoltageResultValueItems(ts time.Time) []*rpc.ResultValueItem { return convResultValueItems(1, ts, sampleIntervals[subTypeIDBatteryVoltage], msg.BatteryVoltage) } func (msg *Message) batteryStateResultValueItems(ts time.Time) []*rpc.ResultValueItem { return convResultValueItems(2, ts, sampleIntervals[subTypeIDBatteryVoltage], msg.BatteryState) } func (msg *Message) pt1000TemperatureResultValueItems(ts time.Time) []*rpc.ResultValueItem { return convResultValueItems(100, ts, sampleIntervals[subTypeIDPT1000Temperature], msg.PT1000Temperature) } func (msg *Message) cpuTemperatureResultValueItems(ts time.Time) []*rpc.ResultValueItem { return convResultValueItems(101, ts, sampleIntervals[subTypeIDCPUTemperature], msg.CPUTemperature) } func (msg *Message) acceleroResultValueItems(ts time.Time) []*rpc.ResultValueItem { return convResultValueItems(102, ts, sampleIntervals[subTypeIDAccelero], msg.Accelero) } func (msg *Message) capTouchResultValueItems(ts time.Time) []*rpc.ResultValueItem { return convResultValueItems(103, ts, sampleIntervals[subTypeIDCapTouch], msg.CapTouch) }