diff --git a/.github/tui.svg b/.github/tui.svg new file mode 100644 index 0000000..a85d0f9 --- /dev/null +++ b/.github/tui.svg @@ -0,0 +1,40 @@ + + + + + + + + n2k command center + + n2k + NMEA 2000 developer console + N2K COMMAND CENTER + 7 workflows + + + + Sniff decoded traffic + Stream typed JSON or physical-value text from CAN, USB, gateway, or capture + + Record raw observations + Capture replayable candump or context-rich observation JSON lines + + Replay a capture + Decode a candump file with optional source timing and CEL filtering + + Validate a source + Summarize typed and undecodable messages; optionally enforce strict mode + + Inventory network devices + Actively discover a live bus or inspect device identities captured in a file + + Explore the PGN schema + Autocomplete known PGNs and inspect fields, ranges, units, and confidence + + Update n2k + Check GitHub releases and update through Homebrew, Go, or a verified binary + + + 1–7 jump • / filter • enter configure • ? help • q quit + diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 03e16db..778f29a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 diff --git a/README.md b/README.md index 7b8a4f6..bae2098 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..70ee5b0 --- /dev/null +++ b/install.sh @@ -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"