nmea

package module
v0.0.0-...-75bc78b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2023 License: Apache-2.0 Imports: 10 Imported by: 0

README

go-nmea-client

WORK IN PROGRESS Only public because including private Go library is too much of a hustle for CI


Go library to read NMEA 2000 messages from SocketCAN interfaces or USB devices (Actisense NGT1/W2K-1 etc).

In addition, this repository contains command line application n2k-reader to provide following features:

  • Can read input from:
    • files
    • TCP connections
    • serial devices
  • Can read different input formats:
    • SocketCAN format
    • CanBoat raw format
    • Actisense format:
      • NGT1 Binary,
      • N2K Ascii,
      • N2K Binary,
      • Raw ASCII
      • EBL (log files from W2K-1 device, NB: NGT1 format is different)
  • Can output read raw frames/messages as:
    • JSON,
    • HEX,
    • BASE64,
    • CanBoat format
  • Can assemble Fast-Packet frames into complete Messages
  • Can decode CAN messages to fields with CanBoat PGN database
  • Can output decoded messages fields as:
    • JSON (stdout)
    • CSV file (each PGN has own csv file). columns (fields) can be customized
  • Can send STDIN input to CAN interface/device
  • Can do basic NMEA2000 bus NODE mapping (which devices/nodes exist in bus)
    • Can list known nodes (send !nodes as input)
    • Can request nodes NAMES from STDIN (send !addr-claim as input)

Disclaimer

This repository exists only because of CanBoat authors. They have done a lot of work to acquire knowledge of NMEA2000 protocol and made it free.

NMEA2000 reader

Compile NMEA2000 reader for different achitectures/platforms (AMD64,ARM32v6,ARM32v7,ARM64,MIPS32 (softfloat)).

make n2kreader-all

Create Actisense reader that can be run on MIPS architecture (Teltonika RUT955 router ,CPU: Atheros Wasp, MIPS 74Kc, 550 MHz)

GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags="-s -w" -o n2k-reader-mips cmd/n2kreader/main.go

Help about arguments:

./n2k-reader -help
Example usage:

Run reader suitable for Raspberry Pi Zero with Canboat PGN database (canboat.json). Only decode PGNs 126996,126998 and output decoded messages as JSON.

./n2kreader-reader-arm32v6 -pgns ./canboat.json -filter 126996,126998 -output-format json
  • You can write data to NMEA bus by sending text to STDIN. Example 6,59904,0,255,3,14,f0,01 + \n sends PGN 59904 from src 0 to dst 255 requesting PGN 126996 (0x01, 0xf0, 0x14)
  • !nodes - lists all knowns node NAME and their associated Source values
  • !addr-claim - sends broadcast request for ISO Address Claim

Read device /dev/ttyUSB0 as ngt format, filter out PGNS 59904,60928 and output decoded messages as json:

./n2k-reader-arm32v6 -pgns canboat.json -input-format ngt -device "/dev/ttyUSB0" -filter 59904,60928 -output-format json

Read file as n2k-ascii format and output decoded messages as json format:

./n2k-reader -pgns=canboat/testdata/canboat.json \
   -device="actisense/testdata/actisense_n2kascii_20221028_10s.txt" \
   -is-file=true \
   -output-format=json \
   -input-format=n2k-ascii

Read file as canboat-raw format, filter out PGNS 127245,127250,129026 and append decoded messages as new lines to CSV files with given fields as columns:

./n2k-reader-amd64 -pgns canboat/testdata/canboat.json \
  -device canboat/testdata/canboat_format.txt \
  -np \
  -is-file \
  -input-format canboat-raw \
  -csv-fields "127245:_time_ms,position,directionOrder;127250:_time_ms(100ms),heading;129026:_time_ms,cog,sog"

This is instructs reader to treat device actisense/testdata/actisense_n2kascii_20221028_10s.txt as an ordinary file instead of serial device. All input read from device is decoded as Actisense N2K binary protocol ( Actisense W2K-1 device can output this) and print output in JSON format.

Read Actisense EBL log file as BST-95 format (created by W2K-1 device) and output decoded messages as json format:

./n2k-reader -pgns=canboat/testdata/canboat.json \
   -device="actisense/testdata/actisense_w2k1_bst95.ebl" \
   -is-file=true \
   -output-format=json \
   -input-format=ebl

Library example

func main() {
	f, err := os.Open("canboat.json")
	schema := canboat.CanboatSchema{}
	if err := json.NewDecoder(f).Decode(&schema); err != nil {
		log.Fatal(err)
	}
	decoder := canboat.NewDecoder(schema)

	// reader, err = os.OpenFile("/path/to/some/logged_traffic.bin", os.O_RDONLY, 0)
	reader, err := serial.OpenPort(&serial.Config{
		Name: "/dev/ttyUSB0",
		Baud: 115200,
		// ReadTimeout is duration that Read call is allowed to block. Device has different timeout for situation when
		// there is no activity on bus. Can not be smaller than 100ms
		ReadTimeout: 100 * time.Millisecond,
		Size:        8,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer reader.Close()

	config := actisense.Config{
		ReceiveDataTimeout:      5 * time.Second,
		DebugLogRawMessageBytes: false,
	}
	// device = actisense.NewN2kASCIIDevice(reader, config) // W2K-1 has support for Actisense N2K Ascii format
	device := actisense.NewBinaryDeviceWithConfig(reader, config)
	if err := device.Initialize(); err != nil {
		log.Fatal(err)
	}

	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()
	for {
		rawMessage, err := device.ReadRawMessage(ctx)
		if err != nil {
			if err == io.EOF || err == context.Canceled {
				return
			}
			log.Fatal(err)
		}
		b, _ := json.Marshal(rawMessage)
		fmt.Printf("#Raw %s\n", b)

		pgn, err := decoder.Decode(rawMessage)
		if err != nil {
			fmt.Println(err)
			continue
		}

		b, _ = json.Marshal(pgn)
		fmt.Printf("%s\n", b)
	}
}

Research/check following:

  1. https://gist.github.com/jackm/f33d6e3a023bfcc680ec3bfa7076e696

Documentation

Index

Constants

View Source
const (
	PGNISORequest               = PGN(59904)  // 0xEA00
	PGNISOAddressClaim          = PGN(60928)  // 0xEE00
	PGNProductInfo              = PGN(126996) // 0x1F014
	PGNConfigurationInformation = PGN(126998) // 0x1F016
	PGNPGNList                  = PGN(126464) // 0x1EE00

	// AddressGlobal is broadcast address used to send messages for all nodes on the n2k bus.
	AddressGlobal = uint8(255)
	// AddressNull is used for nodes that have not or can not claim address in bus. Used with "Cannot claim ISO address" response.
	AddressNull = uint8(254)
)
View Source
const FastRawPacketMaxSize = 223

FastRawPacketMaxSize is maximum size of fast packet multiple packets total length

NMEA200 frame is 8 bytes and to send longer payloads `Fast packet` protocol could be used. In case of fast packet nmea message consist of multiple frames where: * first frame of message has 2 first bytes reserved and up to 6 following bytes for actual payload

  • first byte (data[0]) identifies message counter (first 3 bits) and frame counter (5 bits) for that PGN. Message counter is to distinguish simultaneously sent message frames. Frame counter is always 0 for first frame.
  • second byte (data[1]) indicates message total size in bytes

* second and consecutive frames reserve 1 byte for message counter and frame counter and up to 7 bytes for payload Fast packet maximum payload size 223 comes from the fact that first packet can have only 6 bytes of data and following frames 7 bytes. As frame counter is 5 bits (0-31 dec) we get maximum by 6 + 31 * 7 = 223 bytes.

View Source
const ISOTPDataMaxSize = 1785

Variables

View Source
var (
	// ErrValueNoData indicates that field has no data (for example 8bits uint8=>0xFF, int8=>0x7F)
	ErrValueNoData = errors.New("field value has no data")
	// ErrValueOutOfRange indicates that field value is out of valid range (for example 8bits uint8=>0xFE, int8=>0x7E)
	ErrValueOutOfRange = errors.New("field value out of range")
	// ErrValueReserved indicates that field is reserved (for example 8bits uint8=>0xFD, int8=>0x7D)
	ErrValueReserved = errors.New("field value is reserved")
)

https://www.nmea.org/Assets/2000-explained-white-paper.pdf Page 14 Refers 3 special values "no data", "out of range" and "reserved" https://www.maretron.com/support/manuals/EMS100UM_1.1.html from `EMS100 Engine Monitoring System User's Manual` `Appendix A 'NMEA 2000' Interfacing` Quote "Note: For integer values, the most positive three values are reserved; e.g., for 8-bit unsigned integers, the values 0xFD, 0xFE, 0xFF are reserved, and for 8-bit signed integers, the values 0x7D, 0x7E, 0x7F are reserved. The most positive value (0xFF and 0x7F, respectively, for the 8-bit examples) represents Data Not Available."

Functions

func MarshalRawMessage

func MarshalRawMessage(raw RawMessage) []byte

Types

type Assembler

type Assembler interface {
	Assemble(frame RawFrame, to *RawMessage) bool
}

type CanBusHeader

type CanBusHeader struct {
	PGN         uint32 `json:"pgn"`
	Priority    uint8  `json:"priority"`
	Source      uint8  `json:"source"`
	Destination uint8  `json:"destination"`
}

func ParseCANID

func ParseCANID(canID uint32) CanBusHeader

ParseCANID parses can bus header fields from CANID (29 bits of 32 bit).

func (CanBusHeader) Uint32

func (h CanBusHeader) Uint32() uint32

type EnumValue

type EnumValue struct {
	Value uint32
	Code  string
}

type FastPacketAssembler

type FastPacketAssembler struct {
	// contains filtered or unexported fields
}

func NewFastPacketAssembler

func NewFastPacketAssembler(fpPGNs []uint32) *FastPacketAssembler

func (*FastPacketAssembler) Assemble

func (a *FastPacketAssembler) Assemble(frame RawFrame, to *RawMessage) bool

type FieldValue

type FieldValue struct {
	ID string `json:"id"`
	// normalized to:
	// * string,
	// * float64,
	// * int64,
	// * uint64,
	// * []byte,
	// * time.Duration,
	// * time.Time,
	// * nmea.EnumValue,
	// * [][]nmea.EnumValue <-- for repeating fieldsets/groups
	Value interface{} `json:"value"`
}

FieldValue hold extracted and processed value for PGN field

func (FieldValue) AsFloat64

func (f FieldValue) AsFloat64() (float64, bool)

AsFloat64 converts value to float64 if it is possible.

type FieldValues

type FieldValues []FieldValue

FieldValues is slice of FieldValue

func (FieldValues) FindByID

func (fvs FieldValues) FindByID(ID string) (FieldValue, bool)

type Message

type Message struct {
	// NodeNAME is unique identifier (ISO Address Claim) for Node in NMEA bus.
	//
	// Helps to identify which physical/logical  device/node was author/source of that message. CanBusHeader.Source is
	// not reliable to identify who/what sent the message as source is "randomly" assigned/claimed with ISO address
	// claim process
	//
	// Value `0` means that Node NAME was unknown. For example AddressMapper may have not yet been able to process NAME
	// for that source. For small/fixed NMEA networks this is perfectly fine as you always know what was the actual source
	// for this Message (PGN).
	NodeNAME uint64 `json:"node_name"`

	Header CanBusHeader `json:"header"`
	Fields FieldValues  `json:"fields"`
}

Message is parsed value of PGN packet(s). Message could be assembled from multiple RawMessage instances.

type MessageDecoder

type MessageDecoder interface {
	Decode(raw RawMessage) (Message, error)
}

type PGN

type PGN uint32

type RawData

type RawData []byte

func (*RawData) AsHex

func (d *RawData) AsHex() string

func (*RawData) DecodeBytes

func (d *RawData) DecodeBytes(bitOffset uint16, bitLength uint16, isVariableSize bool) ([]byte, uint16, error)

func (*RawData) DecodeDate

func (d *RawData) DecodeDate(bitOffset uint16, bitLength uint16) (time.Time, error)

func (*RawData) DecodeDecimal

func (d *RawData) DecodeDecimal(bitOffset uint16, bitLength uint16) (uint64, error)

func (*RawData) DecodeFloat

func (d *RawData) DecodeFloat(bitOffset uint16, bitLength uint16) (float64, error)

func (*RawData) DecodeStringFix

func (d *RawData) DecodeStringFix(bitOffset uint16, bitLength uint16) (string, error)

func (*RawData) DecodeStringLAU

func (d *RawData) DecodeStringLAU(bitOffset uint16) (string, uint16, error)

func (*RawData) DecodeStringLZ

func (d *RawData) DecodeStringLZ(bitOffset uint16, bitLength uint16) (string, uint16, error)

func (*RawData) DecodeTime

func (d *RawData) DecodeTime(bitOffset uint16, bitLength uint16, resolution float64) (time.Duration, error)

func (*RawData) DecodeVariableInt

func (d *RawData) DecodeVariableInt(bitOffset uint16, bitLength uint16) (int64, error)

func (*RawData) DecodeVariableUint

func (d *RawData) DecodeVariableUint(bitOffset uint16, bitLength uint16) (uint64, error)

type RawFrame

type RawFrame struct {
	// Time is when frame was read from NMEA bus. Filled by this library.
	Time time.Time

	Header CanBusHeader
	Length uint8 // 1-8
	Data   [8]byte
}

type RawMessage

type RawMessage struct {
	// Time is when message was read from NMEA bus. Filled by this library.
	Time time.Time

	Header CanBusHeader
	Data   RawData // usually 8 bytes but fast-packets can be up to 223 bytes, assembled multi-packets (ISO-TP) up to 1785 bytes
}

RawMessage is complete message that is created from single or multiple raw frames assembled together. RawMessage could be assembled from multiple nmea/canbus frames thus data length can vary up to 1785 bytes.

type RawMessageReader

type RawMessageReader interface {
	ReadRawMessage(ctx context.Context) (msg RawMessage, err error)
	Initialize() error
	Close() error
}

type RawMessageReaderWriter

type RawMessageReaderWriter interface {
	RawMessageReader
	RawMessageWriter
}

type RawMessageWriter

type RawMessageWriter interface {
	WriteRawMessage(ctx context.Context, msg RawMessage) error
	Close() error
}

Directories

Path Synopsis
cmd
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL