Metadata-Version: 2.5
Name: weatherhawk
Version: 1.0.0
Summary: WeatherHawk-232 PakBus and Serial connection CLI - access and management tool to utilize CR2XX without picocom serial pain!
Project-URL: Repository, https://github.com/optimizasean/weatherhawk
Author-email: Sean Harding <optimizasean@gmail.com>
License-Expression: Unlicense
License-File: LICENSE.md
Requires-Python: >=3.14
Requires-Dist: pycampbellcr1000>=0.4
Requires-Dist: pyserial>=3.5
Description-Content-Type: text/markdown

# weatherhawk

A CLI tool for talking to the Campbell Scientific CR200-series datalogger
inside a **WeatherHawk-232** weather station over a USB-serial adapter,
using the real **PakBus/BMP5** protocol (the same one LoggerNet/PC200W
use), with a raw-serial fallback for reading sensor values if PakBus
communication can't be established at all.

Reads live sensor values (temperature, humidity, wind, barometric
pressure, rainfall, solar radiation, ET₀, battery voltage), reads/syncs
the datalogger's clock, and reads/writes site configuration (lat/long/
altitude, barometer calibration offset, rain accumulator reset).

## Hardware

- **Station**: WeatherHawk-232 (direct-connect / wired serial model)
- **Datalogger**: Campbell Scientific CR200-series (`CR200X.Std.04` at
  time of writing), running program `WX-SI-06.CR2`
- **Connection**: USB-to-serial adapter → `/dev/ttyUSB0` (or wherever
  your OS enumerates it) → the WeatherHawk's RS-232 port
- **PakBus baud rate**: 9600 (the only one this station actually
  responds to; higher rates in the retry list are there for other
  CR200-series stations, not this one specifically)

A third-party Crestron SIMPL integration module for the WeatherHawk
([`docs/weatherhawk_weather_station_v9_0.pdf`](docs/weatherhawk_weather_station_v9_0.pdf),
[source](https://applicationmarket.crestron.com/content/Help/WeatherHawk/weatherhawk_weather_station_v9_0.pdf))
independently confirms the RS-232 link parameters this tool uses
(9600 baud, 8 data bits, no parity, 1 stop bit) and documents the same
poll/reset semantics this tool implements over PakBus directly
(`reset-rain` here corresponds to that module's `Reset_Rainfall`
input). It targets `CR200X.Std.02` / program `WH600 V19.CR2`, an older
firmware/program pair than the `CR200X.Std.04` / `WX-SI-06.CR2` this
particular station runs -- included as a useful independent reference
on wiring and protocol basics, not as documentation of this exact
station's program.

No GPS receiver exists anywhere on this station -- confirmed against
both this station's own table definitions (no GPS field, sensor, or
comm port anywhere; see [`docs/table-definitions.md`](docs/table-definitions.md))
and the official WeatherHawk 232 spec sheets (sensor list: wind speed/
direction, air temperature, RH, barometric pressure, solar radiation,
rainfall -- nothing else). `Latitude`/`Longitude`/`Altitude_m` are
static configuration values a human enters once (or via `set-location`
below), not something the station measures itself.

## Why this exists / CR200 vs CR1000 quirks

The natural starting point for talking PakBus in Python is
[`pycampbellcr1000`](https://pypi.org/project/PyCampbellCR1000/), but it
was written against CR1000/CR800/CR3000 dataloggers and has two bugs
that make it fail outright against a CR200:

1. **Wrong timestamp width.** CR1000-family loggers timestamp table
   records with an 8-byte `NSec` (seconds, nanoseconds) value.
   CR200-series loggers (what's actually inside a WeatherHawk-232) use a
   4-byte `Sec` value instead. The table definition the logger reports
   already says which encoding it uses (`TimeType`), but
   `pycampbellcr1000` hardcodes `NSec` everywhere, so every table read
   raises:

   ```text
   struct.error: unpack requires a buffer of 4 bytes
   ```

   [`pakbus_cr200.py`](pakbus_cr200.py)'s `patch_for_cr200()` monkeypatches
   `PakBus.parse_collectdata` to look up the real `TimeType` instead of
   assuming `NSec`. This is the only place this codebase modifies
   `pycampbellcr1000`'s actual decoding logic; everything else (framing,
   signatures, encode/decode helpers, `.TDF` download) is used unmodified.

2. **Broken clock-set math.** `pycampbellcr1000`'s own
   `CR1000.settime()` computes the clock adjustment as a `float` number
   of seconds and passes it straight into an `NSec` field, which needs
   two *integers*:

   ```text
   struct.error: required argument is not an integer
   ```

   `pakbus_cr200.set_time()` reimplements the same clock transaction
   using the library's own `get_clock_cmd()`/`nsec_to_time()` helpers,
   splitting the float diff into `(int seconds, int nanoseconds)` first.

Separately, `pycampbellcr1000` never implements the BMP5 **Get/Set
Values transaction** (`MsgType` `0x1a`/`0x9a` for get, `0x1b`/`0x9b` for
set) -- it only ever reads whole tables. This is the same mechanism
LoggerNet/PC200W use for their `GetValue()`/`SetValue()` calls, and is
documented (and explicitly confirmed CR200-compatible) in Campbell's
BMP5 Transparent Commands manual, section 2.3.5 and section 3.
`pakbus_cr200.get_value()`/`set_value()` implement it directly on top of
the already-open PakBus session, reusing its framing/signature/
`encode_bin`/`decode_bin` logic. This is what `set-location`,
`set-bp-offset`, and `reset-rain` are built on.

One more real-hardware finding worth knowing: the Get/Set Values
transaction only works against the logger's **live** input-location
tables (`Public`, `Status`) -- trying it against a real final-storage
table like `SiteVal` gets back `RespCode 0x10` ("Invalid table or
field"). So confirming a write that lands in `SiteVal` (via
`Public.SaveSite = 1`) requires reading the whole table back with a
Collect Data transaction instead of a second Get Values call. See
`set_location()`/`set_bp_offset()` in `pakbus_cr200.py`.

## Repository layout

```text
src/weatherhawk/
  cli.py                CLI entry point (argparse subcommands)
  pakbus_cr200.py       PakBus/BMP5 protocol layer: the CR200 compatibility
                        patch, connect/table-read helpers, clock get/set,
                        and the Get/Set Values transaction implementation
  serial_fallback.py    Raw serial "5" terminal-mode fallback for reading
                        fields when PakBus can't be reached at all
docs/
  table-definitions.md      Human-readable decode of this station's table
                            definitions (fields, types, read-only flags)
  weatherhawk-232.tdf       The raw .TDF bytes this doc was generated from,
                            captured directly off the physical station
tests/
  conftest.py               Shared fixtures: FakePakBus/FakeDevice, and
                            loaders for the captured fixture bytes below
  fixtures/*.bin            Raw byte payloads captured from the real
                            station (table defs, CollectData responses,
                            a GetValues response) used as "golden" test data
  test_*.py                 104 tests, mocked/fixture-based, no hardware
                            required to run them
```

## Install

Requires Python 3.14+.

The recommended way to install the `weatherhawk` command is as a
[`uv` tool](https://docs.astral.sh/uv/guides/tools/), which installs it
into an isolated environment and puts a `weatherhawk` executable on your
`PATH`:

```bash
uv tool install weatherhawk
```

Or run it without installing anything, via [`uvx`](https://docs.astral.sh/uv/guides/tools/#running-tools):

```bash
uvx weatherhawk fields
```

`pip install weatherhawk` also works if you'd rather manage it in a
regular virtualenv.

You'll also need read/write access to the serial port (typically the
`uucp` or `dialout` group on Linux, depending on distro):

```bash
groups   # check you're in the right group
ls -la /dev/ttyUSB0
```

## Usage

```bash
uv run weatherhawk -h
```

```text
usage: weatherhawk [-h] [--port PORT] [--baud BAUD]
               [--fallback-baud FALLBACK_BAUD] [--timeout TIMEOUT]
               {fields,get-pak,get-serial,status,get-time,set-time,get-location,set-location,set-bp-offset,reset-rain} ...

Talk to the CR200 datalogger inside a WeatherHawk-232 weather station over
PakBus/BMP5, with a raw-serial fallback for reading fields.

positional arguments:
  {fields,get-pak,get-serial,status,get-time,set-time,get-location,set-location,set-bp-offset,reset-rain}
    fields              Read live sensor + config values (PakBus, falls back
                        to raw serial)
    get-pak             Read live sensor + config values over PakBus only (no
                        fallback)
    get-serial          Read live sensor values via the raw serial '5' command
                        only
    status              Read datalogger status (OS version, program, battery,
                        serial number)
    get-time            Read the datalogger's current clock
    set-time            Sync the datalogger's clock to this computer's current
                        time
    get-location        Read the persisted site latitude/longitude/altitude
    set-location        Set site latitude/longitude/altitude (no GPS on this
                        station -- you supply the coordinates)
    set-bp-offset       Calibrate the barometer against a known reference
                        pressure
    reset-rain          Reset the yearly rainfall accumulator

options:
  -h, --help            show this help message and exit
  --port PORT           Serial port device (default: /dev/ttyUSB0)
  --baud BAUD           Force a specific PakBus baud rate instead of trying
                        (9600, 19200, 38400, 57600, 115200) in turn
  --fallback-baud FALLBACK_BAUD
                        Baud rate for the raw serial fallback (default: 9600)
  --timeout TIMEOUT     PakBus connection timeout in seconds (default: 8.0)
```

Run `uv run weatherhawk <command> -h` for help on any specific subcommand.

### Reading values

`fields` is the default day-to-day command: PakBus first, automatic
fallback to the raw serial `'5'` command if PakBus can't be reached at
all.

```console
$ uv run weatherhawk fields
Trying PakBus over serial:/dev/ttyUSB0:9600 ...
============================================================
WeatherHawk-232 fields  (source: PakBus/BMP5 (table 'Public'))
read at: 2026-08-18T20:27:56
============================================================
AirTemp_C            22.92938232421875
Altitude_m           12.0
BPoffset_KPa         0.0
Barometer_KPa        101.7846450805664
BatVolt_V            13.427663803100586
DailyETo_mm          0.0062685501761734486
Int_timer            1.0
Latitude             21.306900024414062
Longitude            -157.85830688476562
RH                   34.61540603637695
RainReset            0.0
RainYearly_mm        0.0
SaveSite             0.0
Solar                1.0989018678665161
WindDirect_deg       344.00604248046875
WindSpeed_ms         9.999999747378752e-06
```

(`~1e-05` on a field like `WindSpeed_ms` is this CR200's zero-ish noise
floor / "unset" placeholder for a `float`, not a real reading, and shows
up on any sensor near zero.)

`get-pak` does the same read but skips the serial fallback and hard-errors
if PakBus fails -- useful for scripting/monitoring where a silent
fallback to a different (much less reliable) data source would be
worse than just failing loudly. `get-serial` does the opposite: skip
PakBus entirely and only try the raw serial command (useful for
debugging the fallback path itself).

`status` reads the `Status` table -- OS version, program name, PakBus
address, battery voltage, serial number:

```console
$ uv run weatherhawk status
...
BattVoltage          13.390727996826172
OSversion            CR200X.Std.04
PakBusAddress        1
ProgName             WX-SI-06.CR2
SerialNum            2851
...
```

(ASCII-type fields like `OSversion`/`OSDate`/`ProgName`/`RfPwrMode` come
back from the datalogger as fixed-width, NUL-padded byte buffers; this
tool truncates at the first NUL and decodes them to plain strings for
display -- see `_decode_ascii_field()` in `pakbus_cr200.py`.)

`get-location` reads back the persisted site configuration from
`SiteVal`.

### Clock

```bash
uv run weatherhawk get-time
uv run weatherhawk set-time      # syncs the datalogger's clock to this computer's clock
```

`set-time` reads the old time, computes the offset, applies it, then
reads back and prints the confirmed new time:

```console
$ uv run weatherhawk set-time
Trying PakBus over serial:/dev/ttyUSB0:9600 ...
Old datalogger time: 2026-08-18 19:42:39
New datalogger time: 2026-08-18 19:42:39
```

### Site configuration (writes)

There is no GPS on this station, so you have to supply real coordinates
yourself (phone GPS, a known site address, etc.):

```bash
uv run weatherhawk set-location 21.3069 -157.8583 --altitude 12
```

```console
Location written and committed to SiteVal:
Altitude_m           12.0
BPoffset_KPa         0.0
Int_timer            1.0
Latitude             21.306900024414062
Longitude            -157.85830688476562
```

Barometer calibration, against a known reference pressure (e.g. a
nearby station's METAR/QFF reading):

```bash
uv run weatherhawk set-bp-offset 0.5
```

Reset the yearly rain accumulator:

```bash
uv run weatherhawk reset-rain
```

`RainReset` is a trigger flag the CR200's ~1-second scan loop has to
notice and act on, so `reset_rain()` waits briefly before reading the
result back -- reading immediately after setting the flag will return
the stale pre-reset total.

## Data reference

See [`docs/table-definitions.md`](docs/table-definitions.md) for the
full field-by-field breakdown of every table this datalogger exposes
(`Status`, `SiteVal`, `data1`, `data2`, `Public`) -- field names, data
types, dimensions, read-only flags, and what each table is actually
for. The raw `.TDF` bytes it was generated from are checked in at
[`docs/weatherhawk-232.tdf`](docs/weatherhawk-232.tdf).

This tool currently only reads/writes `Public`/`Status`/`SiteVal`;
`data1`/`data2` (the logger's historical min/max/avg records) aren't
exposed by the CLI yet but are readable via
`pakbus_cr200.get_table_fields(device, "data1")` if you need them.

## Development

Clone the repo and use [`uv`](https://docs.astral.sh/uv/) to install
`weatherhawk` (editable) plus its runtime dependencies
(`pycampbellcr1000`, `pyserial`) and dev dependencies (`pytest`,
`pytest-cov`, `ruff`, `pyright`) into an in-repo `.venv`:

```bash
uv sync                       # install runtime + dev dependencies
uv run pytest                 # run the test suite (104 tests, no hardware needed)
uv run pytest --cov --cov-report=term-missing   # with coverage
uv run ruff check .           # lint
uv run ruff format .          # format
uv run pyright                # type check
```

The test suite is entirely mocked/fixture-based -- nothing in `tests/`
touches a real serial port. The trickiest decode logic (the CR200
timestamp patch, table definition parsing) is tested against real byte
payloads captured directly from the physical station
(`tests/fixtures/*.bin`), not hand-guessed bytes. See the module
docstring in `tests/conftest.py` for details.

### Linting Markdown

If you edit any `.md` file in this repo, lint it with
[`markdownlint-cli2`](https://github.com/DavidAnson/markdownlint-cli2)
before committing. It's an npm package (not published to PyPI), so it's
not installed via `uv` -- run it directly with `npx`:

```bash
npx markdownlint-cli2 "**/*.md" "#.venv"          # check
npx markdownlint-cli2 --fix "**/*.md" "#.venv"    # auto-fix what it can
```

Rules are configured in [`.markdownlint-cli2.yaml`](.markdownlint-cli2.yaml).

### Publishing a release

Bump `version` in `pyproject.toml` and `__version__` in
`src/weatherhawk/__init__.py`, then build and publish to PyPI:

```bash
uv build             # produces dist/weatherhawk-<version>.tar.gz and .whl
uv publish           # uploads dist/*, prompts for a PyPI API token
```

## References

- [PyCampbellCR1000](https://pypi.org/project/PyCampbellCR1000/) --
  the PakBus/BMP5 client library this project builds on
- Campbell Scientific's BMP5 Transparent Commands manual (the
  authoritative packet-level protocol spec; section 3 covers CR200-
  specific behavior, section 2.3.5 covers the Get/Set Values
  transaction this tool implements on top of the library)
- [`docs/table-definitions.md`](docs/table-definitions.md) -- this
  station's actual table/field layout, decoded from a real `.TDF`
  capture
- [`docs/weatherhawk_weather_station_v9_0.pdf`](docs/weatherhawk_weather_station_v9_0.pdf) --
  a third-party Crestron SIMPL integration module for the WeatherHawk,
  useful as an independent reference on RS-232 wiring/parameters and
  poll/reset semantics ([original source](https://applicationmarket.crestron.com/content/Help/WeatherHawk/weatherhawk_weather_station_v9_0.pdf))

## License

[Unlicense](LICENSE.md) -- public domain. Do whatever you want with
this.
