Metadata-Version: 2.4
Name: pyaltitool
Version: 0.1.0
Summary: Unofficial Python library and CLI for communicating with Alti-2 skydiving altimeters
Author: turbo42
License: MIT
Project-URL: Homepage, https://github.com/5BytesHook/pyaltitool
Project-URL: Repository, https://github.com/5BytesHook/pyaltitool
Project-URL: Issues, https://github.com/5BytesHook/pyaltitool/issues
Keywords: alti-2,altimeter,skydiving,serial,atlas
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Other Audience
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: System :: Hardware
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyserial>=3.5
Dynamic: license-file

# pyaltitool

[![PyPI](https://img.shields.io/pypi/v/pyaltitool)](https://pypi.org/project/pyaltitool/)
[![Python](https://img.shields.io/pypi/pyversions/pyaltitool)](https://pypi.org/project/pyaltitool/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Unofficial Python library and CLI for communicating with Alti-2 skydiving altimeters over USB serial.

| Device            | Status     |
|-------------------|-----------|
| Atlas             | Expected to work |
| Atlas 2           | Tested, works |
| Atlas 2 Student   | Expected to work |
| Juno              | Expected to work |
| MA-12             | Expected to work |
| MA-15A            | Expected to work |

## Features

- Read device information (serial number, firmware version, jump count, etc.)
- Read jump logbook records with full field parsing
- Export logbook to CSV
- Read device date/time
- Read custom name tables (aircraft, drop zones, alarms)
- Read/write raw FRAM memory (**Warning: write operations are untested and may corrupt device data. Use at your own risk!**)
- Read/write device settings (**Warning: writing settings is untested. May cause configuration errors or data loss. Proceed with caution!**)
- Auto port detection, keepalive, and auto-reconnect
- Pure Python — only depends on [pyserial](https://github.com/pyserial/pyserial)
- Automatic logbook date parsing for firmware < 1.0.10 (handles overflow bug).

## Usage

### Getting Started

```bash
git clone https://github.com/5BytesHook/pyaltitool.git
cd pyaltitool
pip install pyserial
```

Connect your Alti-2 device via USB and run:

```bash
python altitool_cli.py
```

The tool auto-detects the serial port. To specify a port manually:

```bash
python altitool_cli.py -p /dev/cu.usbserial-XXXX   # macOS
python altitool_cli.py -p /dev/ttyUSB0              # Linux
python altitool_cli.py -p COM3                      # Windows
```

### Common Commands

Once connected, you'll enter the interactive prompt. Type any command for its usage:

```
altitool> logbook all                    # Show all jumps
altitool> logbook last 10                # Show last 10 jumps
altitool> logbook csv all                # Export all jumps to CSV
altitool> logbook csv last 20 jumps.csv  # Export last 20 to a file
altitool> datetime                       # Read device clock
altitool> names aircraft                 # Read aircraft names
altitool> names dz                       # Read drop zone names
altitool> info                           # Show device info
altitool> help                           # Full command list
```

Quick CSV export without entering interactive mode:

```bash
python altitool_cli.py --csv          # Export all jumps then exit
python altitool_cli.py --csv 50       # Export last 50 jumps then exit
```

---

## Development

### Install for Development

```bash
git clone https://github.com/5BytesHook/pyaltitool.git
cd pyaltitool
pip install -e .
```

### Using as a Library

```python
from pyaltitool import AltitoolDevice, auto_detect_port
from pyaltitool import parse_logbook_record, LOGBOOK_RECORD_SIZE

port = auto_detect_port()  # works on macOS, Linux, Windows
with AltitoolDevice(port) as dev:
    info = dev.connect()
    print(f"{info['product_name']} S/N {info['serial_number']}, {info['total_jumps']} jumps")

    dt = dev.read_datetime()
    print(f"Device time: {dt:%Y-%m-%d %H:%M:%S}")

    addr = info["summary_start"] + (info["total_jumps"] - 5) * LOGBOOK_RECORD_SIZE
    data = dev.read_memory(addr, 5 * LOGBOOK_RECORD_SIZE)

    for i in range(5):
        rec = parse_logbook_record(data[i * 22 : (i + 1) * 22])
        print(f"  Jump #{rec['jump_number']}: {rec['exit_alt_ft']}ft exit, {rec['freefall_time']}s freefall")
```

You can also set the `PYALTITOOL_PORT` environment variable to override auto-detection, or pass the port path directly:

```python
# macOS:   AltitoolDevice("/dev/cu.usbserial-XXXX")
# Linux:   AltitoolDevice("/dev/ttyUSB0")
# Windows: AltitoolDevice("COM3")
```

### API Reference

### `AltitoolDevice`

The main class for communicating with an Alti-2 device.

```python
from pyaltitool import AltitoolDevice, auto_detect_port
```

| Function / Method | Description |
|-------------------|-------------|
| `auto_detect_port() -> str \| None` | Detect the serial port for an Alti-2 device (macOS / Linux / Windows). |

| Method | Description |
|--------|-------------|
| `connect() -> dict` | Open port, wake device, perform handshake. Returns parsed Type 0 record. |
| `disconnect()` | End communication and close port. |
| `reconnect() -> dict` | Re-establish communication with full DTR wake. |
| `force_reconnect()` | Thread-safe reconnection (acquires lock internally). |
| `read_memory(address, length) -> bytes` | Read FRAM memory. Auto-reconnects on failure. |
| `write_memory(address, data)` | Write to FRAM. Splits large writes automatically. **Untested.** |
| `read_datetime() -> datetime` | Read device clock (A2 command). |
| `read_aircraft_names() -> dict[int, str]` | Read aircraft name table from FRAM. |
| `read_dz_names() -> dict[int, str]` | Read drop zone name table from FRAM. |
| `read_alarm_names() -> dict[int, str]` | Read alarm name table from FRAM. |
| `write_aircraft_name(index, name)` | Write an aircraft name. **Untested.** |
| `write_dz_name(index, name)` | Write a drop zone name. **Untested.** |
| `write_alarm_name(index, name)` | Write an alarm name. **Untested.** |
| `read_settings() -> dict` | Read device settings from FRAM. **Untested.** |
| `write_settings(settings, base=None)` | Write settings (read-modify-write). **Untested.** |

Properties: `connected`, `type_zero`, `session_key`.

### Protocol helpers

```python
from pyaltitool import (
    parse_logbook_record,     # Parse 22-byte jump record → dict
    format_logbook_record,    # Format parsed record as human-readable string
    logbook_record_to_csv_row,# Format parsed record as CSV row
    CSV_HEADER,               # CSV header string
    LOGBOOK_RECORD_SIZE,      # 22
    parse_type_zero,          # Parse 32-byte Type 0 record → dict
    parse_datetime_response,  # Parse A2 response → datetime
    parse_settings,           # Parse 13-byte settings → dict
    encode_settings,          # Encode settings dict → 13 bytes
    parse_fram_name,          # Parse 10-byte FRAM name → str
    encode_fram_name,         # Encode str → 10-byte FRAM name
    PRODUCT_NAMES,            # {product_id: name} mapping
)
```

### Exceptions

| Exception | Description |
|-----------|-------------|
| `AltitoolError` | Base exception for all communication errors. |
| `AltitoolAckError(AltitoolError)` | Device returned an unexpected acknowledgement code. |

## How It Works

The Alti-2 devices communicate over USB serial (57600 baud, 8N1, RTS/CTS hardware flow control) using a custom encrypted protocol:

1. **DTR wake** — toggle DTR to wake the device from sleep
2. **ASCII handshake** — send `"018080"` to receive the 32-byte Type 0 identification record
3. **Session key** — derive a 16-byte XTEA key from the Type 0 record + product-specific seed
4. **Encrypted commands** — all subsequent commands are 32-byte XTEA-encrypted packets, sent byte-by-byte with CTS flow control polling
5. **Exit** — send `\x01EXIT` to end the session

The device has a ~10-15 second idle timeout. pyaltitool runs a background keepalive thread that sends periodic datetime pings to prevent disconnection.

For bulk logbook reads, the library automatically reconnects between batches of 100 records, as the device's serial state becomes unreliable during sustained transfers.

See [ALTI2_ATLAS_COMMUNICATION_PROTOCOL.md](ALTI2_ATLAS_COMMUNICATION_PROTOCOL.md) for the full protocol specification.

## License

[MIT](LICENSE)

## Disclaimer

This is an independent open-source project. It is not affiliated with, endorsed by, or supported by Alti-2 Technologies. Use at your own risk. The authors are not responsible for any damage to your altimeter. Always verify your equipment through official channels before skydiving.
