Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/tui.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ jobs:
- name: Test
run: go test ./...

- name: Test release installer
shell: bash
run: |
install_dir="$RUNNER_TEMP/n2k-install"
N2K_INSTALL_DIR="$install_dir" sh ./install.sh
binary=n2k
if [ "$RUNNER_OS" = Windows ]; then
binary=n2k.exe
fi
"$install_dir/$binary" version

race:
runs-on: ubuntu-latest

Expand Down
127 changes: 103 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,142 @@
[![CI](https://github.com/open-ships/n2k-cli/actions/workflows/test.yaml/badge.svg)](https://github.com/open-ships/n2k-cli/actions/workflows/test.yaml)
[![Release](https://img.shields.io/github/v/release/open-ships/n2k-cli)](https://github.com/open-ships/n2k-cli/releases)

`n2k-cli` provides the `n2k` command-line interface for NMEA 2000 (N2K), the
CAN-based network that connects marine instruments such as GPS, depth, wind,
engine, and autopilot systems. It decodes, records, replays, validates, and
inspects traffic from CAN hardware, USB-CAN adapters, WiFi gateways, and
capture files.
**Turn raw NMEA 2000 traffic into typed, searchable, scriptable data.**

Run `n2k` without arguments for a searchable Bubble Tea command center with
guided, autocomplete-enabled workflows. Every operation also remains available
as a stable, non-interactive subcommand for scripts and JSON pipelines.
Marine-network problems often begin as opaque CAN frames: which device sent
this message, what does the payload mean, and can the failure be reproduced?
`n2k` makes that traffic useful without hiding the wire. It combines a guided
Bubble Tea command center with deterministic CLI commands for decoding,
recording, replaying, validating, filtering, and device discovery.

The CLI is powered by the
The CLI is powered by the typed
[`open-ships/n2k`](https://github.com/open-ships/n2k) Go library.

![n2k sniff decoding NMEA 2000 PGNs as typed JSON lines with exact wire values](.github/demo.svg)
## Install

## Quick Start — No Boat Required
Every installation provides the same `n2k` command.

### One-line installer

The repository bundles a real six-second capture from a sailing vessel, so the
first run works at a desk:
Copy and run:

```bash
git clone https://github.com/open-ships/n2k-cli && cd n2k-cli
go run ./cmd/n2k sniff --file testdata/sample.log | jq .
curl -fsSL https://raw.githubusercontent.com/open-ships/n2k-cli/main/install.sh | sh
```

Frames carrying vessel position, routes, or identity were removed from the
sample for privacy.
The installer detects the platform, downloads the matching release, verifies
its SHA-256 checksum, and installs `n2k` into `~/.local/bin` without `sudo`. If
that directory is not already on `PATH`, it prints the exact command to add it.

## Install
| Operating system | Architectures | Shell |
|------------------|---------------|-------|
| macOS | Intel (`amd64`), Apple Silicon (`arm64`) | `sh`, `bash`, or `zsh` |
| Linux | `amd64`, `arm64` | `sh`, `bash`, or `zsh` |
| Windows | `amd64`, `arm64` | Git Bash, MSYS2, or Cygwin |

WSL is detected as Linux. To choose another destination or pin a release:

```bash
curl -fsSL https://raw.githubusercontent.com/open-ships/n2k-cli/main/install.sh \
| N2K_INSTALL_DIR="$HOME/bin" N2K_VERSION=v1.0.2 sh
```

### Homebrew

Install the latest release with Homebrew:
On macOS or Linux:

```bash
brew install --cask open-ships/tap/n2k
n2k version
```

Or install it directly with Go:
Homebrew upgrades are also available directly:

```bash
brew upgrade --cask open-ships/tap/n2k
```

### Go

With Go 1.25.8 or newer:

```bash
go install github.com/open-ships/n2k-cli/cmd/n2k@latest
n2k version
```

Go writes the binary to `GOBIN`, which defaults to `$(go env GOPATH)/bin`;
that directory must be on `PATH`.
make sure that directory is on `PATH`:

```bash
export PATH="$(go env GOPATH)/bin:$PATH"
```

If you set a custom `GOBIN`, `n2k update` preserves it and replaces that same
installation.

Prebuilt binaries for Linux, macOS, and Windows are available on the
### Release binaries

Checksum-protected archives for macOS, Linux, and Windows are published on the
[`n2k-cli` releases page](https://github.com/open-ships/n2k-cli/releases).

To build the CLI from a checkout:
### Build from source

For contributors or local development:

```bash
git clone https://github.com/open-ships/n2k-cli
cd n2k-cli
just build # writes bin/n2k
./bin/n2k --help
just install # optional: installs n2k into Go's bin directory
```

## The `n2k` CLI
## Why `n2k`?

### Guided when you are exploring

Run `n2k` with no arguments. Search the workflow palette, choose a source, and
configure the operation with validated fields and autocomplete. Before
anything touches the network, `n2k` shows the equivalent copyable CLI command.

![The n2k Bubble Tea command center showing searchable decode, record, replay, validate, device, schema, and update workflows](.github/tui.svg)

### Precise when you are automating

The same workflows have stable flags, meaningful exit codes, dynamic shell
completion, CEL filters, and JSON-lines output. Typed values remain paired with
their exact wire representation, so pipelines are convenient without losing
diagnostic fidelity.

![The n2k CLI decoding a sailing-vessel capture into typed JSON lines with PGNs and exact wire values](.github/demo.svg)

### One tool for the whole debugging loop

| Need | What `n2k` provides |
|------|---------------------|
| Inspect traffic now | Decode SocketCAN, USB-CAN, TCP, UDP, candump, and gzip captures. |
| Reproduce a problem | Record owned observations, then replay with original or disabled timing. |
| Reduce the firehose | Filter messages with CEL and select typed, text, JSON, or unknown-PGN output. |
| Find devices | Actively discover a writable network or passively inventory a saved capture. |
| Measure decoder coverage | Validate a source, count undecodable PGNs, and fail CI in strict mode. |
| Understand a PGN | Autocomplete every known PGN and inspect fields, units, ranges, and confidence. |
| Keep tooling current | Update through Homebrew, Go, or checksum-verified release binaries. |

## Quick Start — No Boat Required

The repository includes a privacy-scrubbed, six-second sailing-vessel capture,
so you can try the decoder without CAN hardware:

```bash
git clone --depth 1 https://github.com/open-ships/n2k-cli
cd n2k-cli
n2k sniff --file testdata/sample.log --output text
```

Frames containing vessel position, routes, or identity were removed.

## Rich terminal workflows

### Interactive command center

Expand Down
158 changes: 158 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/bin/sh

set -eu

repository="open-ships/n2k-cli"
program="n2k"

say() {
printf '%s\n' "$*"
}

fail() {
printf 'n2k installer: %s\n' "$*" >&2
exit 1
}

need() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}

curl_https() {
curl --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 "$@"
}

detect_platform() {
case "$(uname -s)" in
Darwin)
os="darwin"
archive_format="tar.gz"
binary_name="$program"
;;
Linux)
os="linux"
archive_format="tar.gz"
binary_name="$program"
;;
MINGW* | MSYS* | CYGWIN*)
os="windows"
archive_format="zip"
binary_name="${program}.exe"
;;
*)
fail "unsupported operating system: $(uname -s)"
;;
esac

case "$(uname -m)" in
x86_64 | amd64)
arch="amd64"
;;
arm64 | aarch64)
arch="arm64"
;;
*)
fail "unsupported architecture: $(uname -m)"
;;
esac
}

latest_version() {
latest_url="$(
curl_https -fsSL \
-o /dev/null \
-w '%{url_effective}' \
"https://github.com/${repository}/releases/latest"
)"
version="${latest_url##*/}"
version="${version#v}"
[ -n "$version" ] || fail "could not determine the latest release"
}

sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "$1" | awk '{print $NF}'
else
fail "sha256sum, shasum, or openssl is required to verify the download"
fi
}

verify_archive() {
expected="$(
awk -v archive="$archive_name" \
'$2 == archive || $2 == ("*" archive) { print $1; exit }' \
"$temporary_dir/checksums.txt"
)"
[ -n "$expected" ] || fail "checksums.txt has no entry for $archive_name"

actual="$(sha256_file "$temporary_dir/$archive_name")"
[ "$actual" = "$expected" ] || fail "SHA-256 verification failed for $archive_name"
}

install_binary() {
case "$archive_format" in
tar.gz)
need tar
tar -xOzf "$temporary_dir/$archive_name" "$binary_name" >"$extracted"
;;
zip)
need unzip
unzip -p "$temporary_dir/$archive_name" "$binary_name" >"$extracted"
;;
esac

[ -s "$extracted" ] || fail "release archive does not contain $binary_name"

mkdir -p "$install_dir"
[ -w "$install_dir" ] || fail "$install_dir is not writable; set N2K_INSTALL_DIR to a writable directory"

staged="$install_dir/.${binary_name}.new.$$"
cp "$extracted" "$staged"
chmod 755 "$staged"
mv -f "$staged" "$install_dir/$binary_name"
}

need curl
need awk
need uname
detect_platform

if [ -n "${N2K_VERSION:-}" ]; then
version="${N2K_VERSION#v}"
else
latest_version
fi

install_dir="${N2K_INSTALL_DIR:-${HOME:?HOME is required}/.local/bin}"
archive_name="${program}_${version}_${os}_${arch}.${archive_format}"
download_root="https://github.com/${repository}/releases/download/v${version}"
temporary_dir="$(mktemp -d "${TMPDIR:-/tmp}/n2k-install.XXXXXX")"
extracted="$temporary_dir/$binary_name"
trap 'rm -rf "$temporary_dir"' 0 HUP INT TERM

say "Installing n2k v${version} for ${os}/${arch}..."
curl_https -fsSL \
-o "$temporary_dir/$archive_name" \
"$download_root/$archive_name"
curl_https -fsSL \
-o "$temporary_dir/checksums.txt" \
"$download_root/checksums.txt"

verify_archive
install_binary

say "Installed $install_dir/$binary_name"
case ":${PATH:-}:" in
*":$install_dir:"*) ;;
*)
say ""
say "Add n2k to your PATH:"
say " export PATH=\"$install_dir:\$PATH\""
;;
esac
say ""
say "Run: n2k --help"