Metadata-Version: 2.4
Name: jetstan-agent
Version: 0.1.14
Summary: Production-grade OTA agent for Linux-based vehicles — MQTT communication backbone.
Author: Nihar Sharma
License: MIT License
        
        Copyright (c) 2026 Nihar Sharma
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Repository, https://github.com/PICODE-Labs/JETSTAN-PICODE
Keywords: ota,mqtt,embedded,automotive,raspberry-pi
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Networking
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: paho-mqtt<3.0.0,>=2.1.0
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
Requires-Dist: fastapi<1.0.0,>=0.110.0
Requires-Dist: uvicorn<1.0.0,>=0.29.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
Dynamic: license-file

# Jetstan Agent

A production-grade OTA agent for Linux-based vehicles. It currently runs
on a Raspberry Pi, but the architecture is designed to move to
automotive Linux hardware without major changes.

It connects securely to HiveMQ Cloud, subscribes to the update topic,
routes incoming commands, and — for `ota.available` — upgrades itself
via `pip` and restarts. See **[INSTALL.md](INSTALL.md)** for running it
as a real systemd service (boot-start, crash/hang recovery, the OTA
update loop).

## Project layout

```
jetstan_agent/
    __init__.py      Package version.
    __main__.py      Enables `python -m jetstan_agent`.
    cli.py            argparse entrypoint (`jetstan-agent` console script).
    config.py         All tunable values and environment-variable loading.
    mqtt.py           TLS connection, subscribe, publish, auto-reconnect.
    agent.py          Wires MQTT to message routing; keeps the process alive.
    message_router.py Parses JSON commands and dispatches by "type".
    updater.py         Self-upgrade via pip + restart-to-apply.
    sd_notify.py       Minimal systemd readiness/watchdog notifications.
    logger.py          Central logging configuration.
    system_checks.py   Read-only root/systemd/distro checks for the installer.
    service_unit.py    Renders the packaged systemd unit template.
    service_cli.py     systemctl/journalctl wrappers.
    installer.py        `jetstan-agent-install` entrypoint.
    uninstaller.py      `jetstan-agent-uninstall` entrypoint.
    templates/          Packaged systemd unit + default agent.env.
tests/
    (one test module per file above)
```

### Why each file exists

- **`config.py`** — the single source of truth for connection settings.
  Non-secret values (port, keepalive, topic) have defaults; the broker
  host, username, and password have none and must come from environment
  variables, so credentials never live in source control.
- **`mqtt.py`** — everything specific to talking to the broker (TLS
  handshake, subscribe, reconnect, systemd readiness/watchdog signaling).
  It knows nothing about OTA — it only hands raw `(topic, payload)` pairs
  to a callback.
- **`message_router.py`** — parses each payload as JSON and dispatches by
  `"type"`. Unknown types and invalid JSON are logged and swallowed, never
  raised — a single bad message can't crash the agent.
- **`updater.py`** — the actual OTA logic: `pip install --upgrade
  jetstan-agent==<version>`, then a deliberate process exit so systemd's
  `Restart=always` relaunches into the newly installed code. No custom
  download/signing pipeline — PyPI already does atomic package installs.
- **`agent.py`** — the composition root. It owns the MQTT client and
  hands incoming payloads to `message_router`.
- **`logger.py`** — configures the root logger once, at startup.
- **`cli.py`** — `run` is the in-process foreground agent (what systemd's
  `ExecStart=` invokes); `status`/`start`/`stop`/`restart`/`logs` are a
  separate concern — they operate on the *systemd service* itself via
  `service_cli.py`, so "start" never means two different things.
- **`installer.py` / `uninstaller.py` / `service_unit.py` /
  `system_checks.py`** — turn `pip install jetstan-agent` into a managed,
  hardened systemd service. See [INSTALL.md](INSTALL.md) for the full
  flow and design rationale.
- **`__main__.py`** — enables `python -m jetstan_agent` (defaults to `run`).

## Configuration

All configuration is via environment variables (read by `config.py`).

| Variable                      | Required | Default                  |
|--------------------------------|----------|---------------------------|
| `MQTT_HOST`                    | Yes      | —                          |
| `MQTT_USERNAME`                 | Yes      | —                          |
| `MQTT_PASSWORD`                 | Yes      | —                          |
| `MQTT_PORT`                     | No       | `8883`                     |
| `MQTT_TOPIC`                    | No       | `jetstan/demo/update`      |
| `JETSTAN_MQTT_CLIENT_ID`       | No       | `jetstan-agent`            |
| `JETSTAN_LOG_LEVEL`            | No       | `INFO`                     |

In production (systemd on the vehicle, CI, etc.) set these as real
environment variables — e.g. a systemd unit's `EnvironmentFile=` —
never hardcode them in a file that gets committed.

### Local development: `.env` file

For local development, `config.py` automatically loads a `.env` file
from the project root (via `python-dotenv`) before reading any
configuration — no manual `export` needed, and no code changes.

```bash
cp .env.example .env
# then edit .env and fill in MQTT_HOST / MQTT_USERNAME / MQTT_PASSWORD
```

`.env` is git-ignored, so credentials never get committed. If a real
environment variable is already set, it takes precedence over `.env` —
so this has no effect on production deployments where `.env` won't
even exist.

## Arduino firmware updates

`software/arduino_updates/` extends the OTA pipeline to also flash an
Arduino connected to the Pi over USB, as one more step of every agent
startup (which includes right after every self-update, since that ends
in a restart):

```
Agent update finishes -> Check firmware -> Flash Arduino (if changed) -> Verify -> Continue startup
```

It's opt-in and off by default. Add to `/etc/jetstan/agent.env` (or the
local `.env` for development):

| Variable                        | Required | Default                      |
|----------------------------------|----------|-------------------------------|
| `ARDUINO_ENABLED`                | No       | `false`                       |
| `ARDUINO_BOARD`                  | No       | `uno`  (also: `nano`, `mega2560`) |
| `ARDUINO_PORT`                   | No       | `auto` (or an explicit `/dev/tty...`) |
| `ARDUINO_BAUD`                   | No       | `115200`                      |
| `ARDUINO_FIRMWARE_PATH`          | No       | `/etc/jetstan/firmware.hex`   |
| `ARDUINO_STATE_DIR`              | No       | `/var/lib/jetstan`             |
| `ARDUINO_FLASH_RETRIES`          | No       | `3`                            |
| `ARDUINO_FLASH_TIMEOUT_SECONDS`  | No       | `60`                           |

Requires `avrdude` on the Pi (`sudo apt install avrdude`) and the service
account in the `dialout` group — the installer handles the group
membership automatically; see [INSTALL.md](INSTALL.md).

How it decides whether to flash: the firmware at `ARDUINO_FIRMWARE_PATH`
is fingerprinted (SHA-256 of its bytes), and that fingerprint is compared
against the last one successfully flashed (persisted under
`ARDUINO_STATE_DIR`) — so redeploying the same `firmware.hex` is always a
no-op, and any real change is always picked up, with no manually-tracked
version number to forget to bump.

Module layout, matching the rest of this repo's one-file-one-concern style:

- **`exceptions.py`** — one `ArduinoUpdateError` subclass per failure
  mode (board missing, permission denied, timeout, verification
  mismatch, ...), so callers/logs are specific about what went wrong.
- **`config.py`** — `ArduinoConfig.from_env()`, plus the board-name ->
  avrdude-parameters registry (`BoardProfile`).
- **`discovery.py`** — finds `/dev/ttyUSB*`/`/dev/ttyACM*` device nodes
  and resolves which one to use; refuses to guess if there's more than
  one and none is configured explicitly.
- **`firmware.py`** — content-fingerprints a `.hex` file and persists
  the last-flashed fingerprint (atomic write, same pattern as
  `installer.py`).
- **`flasher.py`** — an abstract `BoardFlasher` interface plus
  `AVRDudeFlasher`. Adding STM32/ESP32/RP2040 support later means
  adding another `BoardFlasher` subclass and a `FLASHERS` entry —
  nothing in the OTA pipeline changes.
- **`verifier.py`** — runs the board-specific verify step and turns a
  mismatch into a `VerificationError`.
- **`__init__.py`** — `check_and_flash()` (raises on failure) and
  `sync_firmware_safe()` (never raises — what `jetstan_agent/cli.py`
  actually calls before starting the MQTT loop).
- **`sample_firmware/motor_test/`** — a minimal sketch (drives a motor
  driver's RPWM/LPWM pins at ~25% duty cycle from boot, nothing else)
  for validating the flashing pipeline end-to-end without needing real
  application firmware yet. See its `sample_firmware/README.md` for how
  to compile it.

## Install locally (editable)

```bash
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
```

## Run

Either export the variables directly:

```bash
export MQTT_HOST="xxxxxxxx.s1.eu.hivemq.cloud"
export MQTT_USERNAME="your-username"
export MQTT_PASSWORD="your-password"
```

or, for local development, create a `.env` file as shown above and skip
the `export` lines entirely. Then:

```bash
python -m jetstan_agent
# or, equivalently:
jetstan-agent run
```

On startup the agent connects over TLS, subscribes to
`jetstan/demo/update`, and logs every message it receives. If the
connection drops (or the broker is briefly unreachable), it reconnects
automatically with backoff — no restart needed.

## Test connectivity

1. Run the agent as shown above and leave it running.
2. From the [HiveMQ Cloud console](https://console.hivemq.cloud) (or
   any MQTT client, e.g. `mosquitto_pub`), publish a message to
   `jetstan/demo/update`:

   ```bash
   mosquitto_pub -h <your-cluster-host> -p 8883 --cafile /etc/ssl/certs/ca-certificates.crt \
     -u <username> -P <password> -t jetstan/demo/update -m '{"version": "1.0.1"}'
   ```

3. Confirm the agent's log output prints the received message.

## Run the test suite

```bash
pytest
```
