Sending engine RPMs to the NMEA bus, 2/x

It turns out that iNavX doesn’t support rendering ERRPM; it only supports the NMEA 2000 variant, PGN 127488. This led me down the path of trying to work out if I could send PGNs to the MiniPlex3 via the WiFi interface.

The answer is sort of yes – you can send a specially crafted 0183 sentence starting $MXPGN, and encode the PGN inside that sentence. The MiniPlex3 will unpack the data, and send it to the NMEA 2000 bus. It will also send the encoded sentence back out on the WiFi and USB port, but it doesn’t send the decoded packet ๐Ÿ™

However, Imray’s Boat Instruments application will happily read ERRPM and render it. So iNavX is out the door for this particular aspect, and I can just use the instruments app instead.

Managed to introduce a bug as I refactored the code, dropping the “\r\n” from the packet being sent to the MiniPlex3. The multiplexer does not tell you that it’s dropping the packet, and then drops it, and nothing shows on the console.

Updated code for the sender.

#!/usr/bin/env python3

import random
import re
import socket
import time
from typing import Tuple


class NMEASender:
    def __init__(
        self, connection_type: socket.SocketKind, remote_host: str, remote_port: int
    ) -> None:
        self.host = remote_host
        self.port = remote_port
        self.conn_type = connection_type

    def checksum_for(self, sentence: str) -> str:
        """
        Sample sentence is $XXXXX,data,goes,here*
        No checksum present, this calculates it.
        """
        chksumdata = re.sub(
            "(\n|\r\n)", "", sentence[sentence.find("$") + 1 : sentence.find("*")]
        )
        csum = 0
        for c in chksumdata:
            csum ^= ord(c)
        # Mangle the result appropriately
        return str(hex(csum)).lstrip("0x").upper()

    def send_errpm(self, source: str, engine_number: int, rpm: int, pitch: int) -> None:
        """
        Sends engine RPM as a NMEA 0183 sentence.
        """
        if source not in ("E", "S"):
            raise Exception("Source must be E(ngine) or S(haft)")
        if engine_number not in (0, 1, 2):
            raise Exception(
                "Engine number must be 0 (centre), 1 (port) or 2 (starboard)"
            )
        if rpm < 0:
            raise Exception("RPM cannot be negative")
        if pitch < -100 or pitch > 100:
            raise Exception("Pitch is a percentile number between -100 and 100")
        self.send_0183_sentence(
            self.compose_sentence(
                talker="ERRPM", values=(source, engine_number, rpm, pitch, "A")
            )
        )

    def compose_sentence(self, talker: str, values: Tuple) -> str:
        """
        Given a talker name and a tuple of values, build a sentence of the form 
        $TALKR,val1,val2,...,valN*CK
        """
        s = f"${talker},{','.join([str(x) for x in values])}*"
        return f"{s}{self.checksum_for(s)}"

    def send_0183_sentence(self, sentence: str) -> None:
        # NMEA 0183 sentences must
        # * start with $
        # * contain comma-delimited values
        # * carry a * marker after the values
        # * have a hex checksum after the *
        # * end with \r\n
        # For python to be happy with sockets, encode as bytestring
        if not sentence.startswith("$"):
            raise Exception("Sentence does not start with a $")
        if not "*" in sentence:
            raise Exception("Sentence does not contain a *")

        with socket.socket(socket.AF_INET, self.conn_type) as s:
            print(f"Sending {sentence} to {self.host}:{self.port}")
            if s.type == socket.SocketKind.SOCK_DGRAM:
                s.sendto(f"{sentence}\r\n".encode(), (self.host, self.port))
            elif s.type == socket.SocketKind.SOCK_STREAM:
                s.connect((self.host, self.port))
                s.sendall(f"sentence\r\n".encode())
            else:
                raise Exception("Expecting DGRAM or STREAM socket")


if __name__ == "__main__":
    sender = NMEASender(
        connection_type=socket.SOCK_DGRAM, remote_host="10.0.0.1", remote_port=10110
    )
    while True:
        rpm = random.randint(1000, 3400)
        sender.send_errpm(source="E", engine_number=0, rpm=rpm, pitch=100)
        time.sleep(2)