Metadata-Version: 2.4
Name: rpi-hw-lock
Version: 0.1.0
Summary: 同じGPIO/SPI/I2C/UARTを使う複数プログラム間で、systemdサービスを一時停止して排他アクセスを確保するためのコンテキストマネージャ
Author: Kazuki Kumahata
License: MIT
Project-URL: Homepage, https://github.com/kazuki1729/rpi-hw-lock
Project-URL: Issues, https://github.com/kazuki1729/rpi-hw-lock/issues
Keywords: raspberry-pi,systemd,gpio,spi,i2c,uart,lock,mutex,hardware
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: System :: Hardware
Classifier: Intended Audience :: Developers
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# rpi-hw-lock

Raspberry Pi上で、複数のプログラムが同じGPIO/SPI/I2C/UARTを取り合う問題を解決するための、
軽量なPythonライブラリです。

## 背景・課題

Raspberry Pi上で常時稼働しているセンサー収集サービス（systemdサービス）がある状態で、
別の新しい実験用スクリプトが同じセンサー配線（GPIO/SPI/I2Cバス）を使いたい場合、
両方のプロセスが同時にハードウェアへアクセスすると、通信内容が破損する恐れがあります。
I2C・SPIは複数ステップからなるトランザクションであり、カーネルドライバはこれを
プロセスをまたいで排他制御してはくれません。

これまでは、新しいスクリプトを動かす前に手動で`sudo systemctl stop`し、終わったら
`sudo systemctl start`し忘れないようにする、という運用でカバーしていました。

## 解決方法

対象のsystemdサービスを**一時的に完全停止**させ、処理が終わったら（例外が発生しても）
自動的に元の状態へ復元する、Pythonのコンテキストマネージャを提供します。

ロックファイル等の「お互いに譲り合う」方式ではなく、対象プロセスを完全に停止させる
方式なので、**既存のサービス側のコードを一切変更する必要がありません**。

## インストール

```bash
pip install rpi-hw-lock
```

依存パッケージはありません（Python標準ライブラリのみで動作します）。

## 使い方

```python
from rpi_hw_lock import exclusive_hardware_access

with exclusive_hardware_access(["sensor-tiered-client.service"]):
    # ここでGPIO/SPI/I2C/UARTを自由に使うコード
    ...
# withブロックを抜けた時点(例外が発生していても)、元々activeだったサービスだけが
# 自動的に再開されます
```

複数のサービスをまとめて一時停止することもできます。

```python
with exclusive_hardware_access(["service-a.service", "service-b.service"]):
    ...
```

### 事前準備: sudoersの設定

`systemctl stop/start`には通常sudoが必要です。パスワード入力を求められないよう、
**対象サービスのstop/startだけに絞った**sudoersルールを1回設定してください。

```bash
sudo visudo -f /etc/sudoers.d/rpi-hw-lock
```

以下のような内容を記載します（`pi`と`sensor-tiered-client.service`は環境に合わせて変更）。

```
pi ALL=(root) NOPASSWD: /usr/bin/systemctl stop sensor-tiered-client.service
pi ALL=(root) NOPASSWD: /usr/bin/systemctl start sensor-tiered-client.service
```

`systemctl`全体ではなく、対象サービスの`stop`/`start`だけに限定することで、
意図しない権限昇格を防いでいます。

### 事前確認: 権限が正しく設定されているか

実際に停止する前に、パスワード無しで実行できる権限が設定されているかを確認できます。

```python
from rpi_hw_lock import check_permissions

check_permissions(["sensor-tiered-client.service"])  # 権限が無ければPermissionErrorを送出
```

## API

| 関数 | 説明 |
| :--- | :--- |
| `exclusive_hardware_access(services, timeout=10.0, verify_stopped=True)` | コンテキストマネージャ本体。`services`は文字列1つ、またはリスト |
| `check_permissions(services, timeout=5.0)` | sudoersの設定状況を事前確認する |
| `is_active(service)` | 指定したサービスが現在activeかどうかを返す |
| `ServiceControlError` | systemctlの実行に失敗した場合の例外 |

## 設計上の注意

- 対象サービスに`Restart=on-failure`のような自動再起動設定があっても、`systemctl stop`
  による明示的な停止は自動再起動の対象になりません（systemdの仕様）。停止中に勝手に
  サービスが復活することはありません。
- `verify_stopped=True`（既定）の場合、停止コマンド実行後に実際にinactiveへ変わったかを
  再確認します。想定外の状態であれば`ServiceControlError`を送出します。
- 元々inactiveだったサービスは、`with`ブロックを抜けても起動されません（意図的に
  止めていたサービスを誤って起動してしまう事故を防ぐため）。
- 再起動(`start`)に失敗した場合、`with`ブロック内で発生した例外を隠さないよう、
  再起動失敗はログに記録するのみで、例外としては送出しません（両方を送出すると
  元の例外が握りつぶされてしまうため）。

## ライセンス

MIT
