src.dualinventive.com/go/nbiot-interface/internal/device/crm3000/message.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)
}