343 lines
9.1 KiB
Go
343 lines
9.1 KiB
Go
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)
|
|
}
|