Changed some defaults and warn on bad settings. Added bazzite support for setup.sh USB perms.
This commit is contained in:
parent
47bc23cd16
commit
b1a405e25d
4 changed files with 245 additions and 66 deletions
130
README.md
130
README.md
|
|
@ -128,12 +128,42 @@ Idempotent. It installs a udev rule, creates `.venv`, and installs pyusb. It
|
||||||
needs `sudo` for the udev rule only.
|
needs `sudo` for the udev rule only.
|
||||||
|
|
||||||
The rule is needed because the CH347's USB node defaults to `root:root 0664`.
|
The rule is needed because the CH347's USB node defaults to `root:root 0664`.
|
||||||
It grants the `plugdev` group access:
|
It grants access two ways, because no single mechanism covers every distro:
|
||||||
|
|
||||||
```
|
```
|
||||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55db", GROUP="plugdev", MODE="0660"
|
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55db", TAG+="uaccess", GROUP="plugdev", MODE="0660"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **`TAG+="uaccess"`** makes systemd-logind put an ACL on the node for whoever
|
||||||
|
is logged in at the local seat. No group, no logout, and it is the only part
|
||||||
|
that works unaided on atomic Fedora. `setup.sh` triggers with
|
||||||
|
`--action=add`, not the default `change`, because the `uaccess` builtin only
|
||||||
|
runs on add — a change event installs the rule but grants nothing until the
|
||||||
|
next replug.
|
||||||
|
- **`GROUP=`/`MODE=`** is the fallback for ssh sessions and seatless systems,
|
||||||
|
where there is no local seat for `uaccess` to grant to. The group is chosen
|
||||||
|
from those that exist — `plugdev` is a Debian convention and absent on
|
||||||
|
Fedora, and naming a group that does not exist makes udev log an error and
|
||||||
|
leave the node owned by root. If neither `plugdev` nor `dialout` exists the
|
||||||
|
clause is omitted entirely.
|
||||||
|
|
||||||
|
#### Atomic Fedora (Bazzite, Silverblue, Kinoite)
|
||||||
|
|
||||||
|
`usermod -aG dialout $USER` fails there with *"group 'dialout' does not
|
||||||
|
exist"* even though `getent group dialout` finds it. Fedora ships
|
||||||
|
`nss_altfiles`, so `getent` reads both `/etc/group` and `/usr/lib/group`, while
|
||||||
|
`usermod` writes `/etc/group` alone — and on an rpm-ostree system the group
|
||||||
|
frequently lives only in the latter. Copy the line across first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -E '^dialout:' /usr/lib/group | sudo tee -a /etc/group
|
||||||
|
sudo usermod -aG dialout $USER # then log out and back in
|
||||||
|
```
|
||||||
|
|
||||||
|
None of that is needed for a local desktop session: `uaccess` covers it, which
|
||||||
|
is why the rule carries both. `setup.sh` tests the node itself at the end and
|
||||||
|
prints whichever fix applies rather than guessing up front.
|
||||||
|
|
||||||
Python 3.14 here is PEP 668 externally-managed, hence the venv rather than a
|
Python 3.14 here is PEP 668 externally-managed, hence the venv rather than a
|
||||||
system-wide `pip install`.
|
system-wide `pip install`.
|
||||||
|
|
||||||
|
|
@ -147,12 +177,13 @@ system-wide `pip install`.
|
||||||
|
|
||||||
| Flag | Default | Meaning |
|
| Flag | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `--cycle-count` | 50 | sets the rate and the LSB; see Recommended configuration |
|
| `--rate` | — | target Hz; derives the cycle count and TMRC. Mutually exclusive with `--cycle-count` |
|
||||||
|
| `--cycle-count` | 100 | sets the rate and the LSB; see Recommended configuration |
|
||||||
| `--tmrc` | fastest | rate register. Default lets the cycle count set the rate; give one only to sample *slower* |
|
| `--tmrc` | fastest | rate register. Default lets the cycle count set the rate; give one only to sample *slower* |
|
||||||
| `--duration` | 0 | seconds, 0 = until Ctrl-C |
|
| `--duration` | 0 | seconds, 0 = until Ctrl-C |
|
||||||
| `--output` | timestamped | CSV path |
|
| `--output` | timestamped | CSV path |
|
||||||
| `--address` | autodetect | skip the scan |
|
| `--address` | autodetect | skip the scan |
|
||||||
| `--bus-speed` | 750 | I²C kHz; 100 would spend 80% of the period on the bus at cc=50 |
|
| `--bus-speed` | 750 | I²C kHz; 100 would spend 42% of the period on the bus at cc=100 |
|
||||||
| `--scan-only` | — | scan and exit |
|
| `--scan-only` | — | scan and exit |
|
||||||
|
|
||||||
Exit status is non-zero if an interval was missed. The partial capture is kept
|
Exit status is non-zero if an interval was missed. The partial capture is kept
|
||||||
|
|
@ -440,7 +471,7 @@ Symptoms map cleanly onto causes, so work down this list.
|
||||||
|
|
||||||
| Symptom | Cause |
|
| Symptom | Cause |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Cannot claim CH347 interface 2: Access denied` | udev rule missing — run `./setup.sh`. Do **not** use `sudo`: it works as your normal user, and running as root leaves root-owned capture files |
|
| `Cannot claim CH347 interface 2: Access denied` | udev rule missing or not yet applied to this node — run `./setup.sh`, which diagnoses which of `uaccess` or group membership fell through. Do **not** reach for `sudo`: it works as your normal user, and masking a permissions problem with root only defers it. `--high-priority` is the one case that legitimately needs root, and it hands the capture file back to `$SUDO_UID` afterwards |
|
||||||
| `No CH347 adapter found` | not plugged in, or not in Mode 1 |
|
| `No CH347 adapter found` | not plugged in, or not in Mode 1 |
|
||||||
| `ABORTED: missed N interval(s)` | the host could not keep up — raise `--tmrc` (slower) or `--bus-speed`. Expected at cycle count 50 near the sensor's maximum |
|
| `ABORTED: missed N interval(s)` | the host could not keep up — raise `--tmrc` (slower) or `--bus-speed`. Expected at cycle count 50 near the sensor's maximum |
|
||||||
| `no capture header found` | a capture predating the header format — re-record it |
|
| `no capture header found` | a capture predating the header format — re-record it |
|
||||||
|
|
@ -743,30 +774,37 @@ better and 750 kHz is the default everywhere.
|
||||||
|
|
||||||
### Recommended configuration
|
### Recommended configuration
|
||||||
|
|
||||||
**Cycle count 50 at 750 kHz**, which is what the defaults now do. That runs the
|
**Cycle count 100 at 750 kHz**, which is what the defaults do. That runs the
|
||||||
sensor at its own ceiling of ~534 Hz, with TMRC held fast so the cycle count
|
sensor at its own ceiling of ~282 Hz, with TMRC held fast so the cycle count
|
||||||
governs and the duty cycle stays near 100%.
|
governs and the duty cycle stays at 94%.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./.venv/bin/python logger.py --duration 60 # cc=50, 750 kHz, ~534 Hz
|
./.venv/bin/python logger.py --duration 60 # cc=100, 750 kHz, ~282 Hz
|
||||||
```
|
```
|
||||||
|
|
||||||
Against cycle count 200 this costs **4.4%** in post-filter noise — 89% duty
|
It sits deliberately between the two things that pull in opposite directions:
|
||||||
against 97%, and nothing else — while giving **3.7x the spectrum**, 267 Hz of
|
|
||||||
Nyquist against 73 Hz. That trade is worth taking because **aliased interference
|
|
||||||
cannot be filtered out afterwards at any cycle count**, so seeing it beats a few
|
|
||||||
percent of noise. At 534 Hz mains and its first four harmonics all sit in band
|
|
||||||
and can be notched; at 145 Hz only the 60 Hz fundamental does, and the rest fold
|
|
||||||
irreversibly onto signal.
|
|
||||||
|
|
||||||
750 kHz is not optional at this cycle count. The period is 1.873 ms, and the
|
- **Against cycle count 200** it costs **1.5%** in post-filter noise — 94% duty
|
||||||
|
against 97%, and nothing else — for **1.9× the spectrum**, 141 Hz of Nyquist
|
||||||
|
against 73 Hz. Worth taking, because **aliased interference cannot be filtered
|
||||||
|
out afterwards at any cycle count**, so seeing it beats 1.5% of noise. At
|
||||||
|
282 Hz both mains and its second harmonic sit in band and can be notched; at
|
||||||
|
145 Hz only the fundamental does.
|
||||||
|
- **Against cycle count 50** it gives up half the spectrum (141 Hz of Nyquist
|
||||||
|
against 267 Hz) and buys **36% more dither margin**, 0.79 LSB against 0.58,
|
||||||
|
plus a 1.9× coarser rate demand on the host. cc=50 is the right choice when
|
||||||
|
something above 141 Hz needs identifying; it is not the right default,
|
||||||
|
because its dither margin is the one assumption in the whole configuration
|
||||||
|
that has not yet been checked against a real quiet-site capture.
|
||||||
|
|
||||||
|
750 kHz is not optional at this cycle count. The period is 3.539 ms, and the
|
||||||
irreducible I2C traffic per sample is:
|
irreducible I2C traffic per sample is:
|
||||||
|
|
||||||
| bus | traffic | share of period |
|
| bus | traffic | share of period |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 100 kHz | 1.500 ms | **80%** |
|
| 100 kHz | 1.500 ms | **42%** |
|
||||||
| 400 kHz | 0.375 ms | 20% |
|
| 400 kHz | 0.375 ms | 11% |
|
||||||
| 750 kHz | 0.200 ms | **11%** |
|
| 750 kHz | 0.200 ms | **6%** |
|
||||||
|
|
||||||
750 kHz is also the fastest measured, not merely the least bus traffic. Host
|
750 kHz is also the fastest measured, not merely the least bus traffic. Host
|
||||||
cost per sample, three runs each at cycle count 228:
|
cost per sample, three runs each at cycle count 228:
|
||||||
|
|
@ -783,19 +821,47 @@ benchmarked faster than 750; that rested on a single short run whose 4%
|
||||||
difference was inside the run-to-run spread, and does not survive repetition.
|
difference was inside the run-to-run spread, and does not survive repetition.
|
||||||
400 kHz remains a sane fallback if 750 proves marginal on long wiring.
|
400 kHz remains a sane fallback if 750 proves marginal on long wiring.
|
||||||
|
|
||||||
**Fall back to cycle count 100 or 200 if dither is thin.** The one assumption
|
**Fall back to cycle count 200 if dither is still thin.** The assumption to check
|
||||||
worth checking is that cc=50 stays dithered: its intrinsic noise is 0.58 LSB,
|
is that cc=100 stays dithered: its intrinsic noise is 0.79 LSB against a
|
||||||
and simulation puts the failure point below ~0.2 LSB, so there is roughly 3x of
|
simulated failure point below ~0.2 LSB, so there is roughly 4× of margin at the
|
||||||
margin. `characterize.py` prints `sd/LSB` per axis, which settles it from the
|
spec noise figure. That margin shrinks if the real sensor is quieter than spec,
|
||||||
first capture — below ~0.35 and averaging stops recovering sub-LSB resolution,
|
which is the entire point of the quiet-site experiment.
|
||||||
at which point 100 (1.17 LSB) or 200 (1.10 LSB) buys the margin back at the cost
|
`characterize.py` prints `sd/LSB` per axis, which settles it from the first
|
||||||
of bandwidth.
|
capture — below ~0.35 and averaging stops recovering sub-LSB resolution, at
|
||||||
|
which point 200 (1.10 LSB) buys the margin back at the cost of bandwidth.
|
||||||
|
|
||||||
| cycle count | rate | Nyquist | nT/LSB | dither | duty |
|
| cycle count | rate | Nyquist | nT/LSB | dither | duty | |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| **50** | **534 Hz** | **267 Hz** | 50.37 | **0.58** | 89.0% |
|
| 50 | 534 Hz | 267 Hz | 50.37 | 0.58 | 89.0% | spectrum, thin dither |
|
||||||
| 100 | 281 Hz | 141 Hz | 26.17 | 0.80 | 93.7% |
|
| **100** | **282 Hz** | **141 Hz** | **26.17** | **0.79** | **94.2%** | **default** |
|
||||||
| 200 | 145 Hz | 73 Hz | 13.35 | 1.10 | 97.0% |
|
| 200 | 145 Hz | 73 Hz | 13.35 | 1.10 | 97.0% | resolution, 60 Hz only |
|
||||||
|
|
||||||
|
`logger.py` warns on stderr for anything below cycle count 50
|
||||||
|
(`rm3100.RECOMMENDED_MIN_CYCLE_COUNT`), along with five other configuration
|
||||||
|
traps — see [Configuration warnings](#configuration-warnings).
|
||||||
|
|
||||||
|
### Configuration warnings
|
||||||
|
|
||||||
|
`rm3100.plan()` resolves a configuration and `logger.py` prints its derivation
|
||||||
|
before every run, then checks it against six known traps. Each one is a *silent*
|
||||||
|
failure: the capture completes, the numbers look plausible, and the defect is
|
||||||
|
only visible afterwards. So each is reported on stderr and **nothing is fixed
|
||||||
|
automatically** — changing a setting that was asked for would hide the problem
|
||||||
|
behind a configuration change.
|
||||||
|
|
||||||
|
| Warning | Trigger | Why it matters |
|
||||||
|
|---|---|---|
|
||||||
|
| cycle count below the recommended 50 | `cc < RECOMMENDED_MIN_CYCLE_COUNT` | dither thins toward the ~0.2 LSB where averaging stops recovering sub-LSB resolution |
|
||||||
|
| TMRC governs and the sensor idles | TMRC-governed and idle > 20% | idle time buys nothing; measured 1.43× the ASD at 23% duty |
|
||||||
|
| rate differs from the one requested | \|error\| > 2% | the run silently uses the ceiling, and every figure above it is derived from the rate |
|
||||||
|
| Nyquist below 60 Hz | rate < 120 Hz | mains folds onto signal and no later filter undoes it |
|
||||||
|
| cycle count past 400 | `cc > MAX_SPEC_CYCLE_COUNT` | Table 3-1 ends there, so the printed gain and noise are extrapolated, not specified |
|
||||||
|
| bus over half the period | traffic / period > 50% | names the speed that would fit |
|
||||||
|
|
||||||
|
The aliasing one is the easiest to walk into: `--rate 32` yields a clean-looking
|
||||||
|
2.91 nT/LSB capture with 60 Hz mains sitting at 4.02 Hz, indistinguishable from
|
||||||
|
signal. Sampling fast and decimating afterwards gives the same noise floor with
|
||||||
|
the line still visible.
|
||||||
|
|
||||||
### Rate model, and how TMRC is chosen
|
### Rate model, and how TMRC is chosen
|
||||||
|
|
||||||
|
|
|
||||||
74
logger.py
74
logger.py
|
|
@ -23,6 +23,7 @@ import csv
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -33,6 +34,10 @@ import rm3100
|
||||||
CSV_FIELDS = ["sample_index", "system_time_unix", "x_raw", "y_raw", "z_raw",
|
CSV_FIELDS = ["sample_index", "system_time_unix", "x_raw", "y_raw", "z_raw",
|
||||||
"warning"]
|
"warning"]
|
||||||
|
|
||||||
|
# Mains fundamental. Anything sampled below twice this folds the line onto
|
||||||
|
# signal irreversibly, which is the one error post-processing cannot undo.
|
||||||
|
MAINS_HZ = 60.0
|
||||||
|
|
||||||
BUS_SPEEDS = {
|
BUS_SPEEDS = {
|
||||||
20: ch347.SPEED_20KHZ,
|
20: ch347.SPEED_20KHZ,
|
||||||
100: ch347.SPEED_100KHZ,
|
100: ch347.SPEED_100KHZ,
|
||||||
|
|
@ -179,8 +184,8 @@ def parse_args():
|
||||||
help="I2C address, skipping the scan (default: autodetect)")
|
help="I2C address, skipping the scan (default: autodetect)")
|
||||||
p.add_argument("--bus-speed", type=int, choices=[20, 100, 400, 750], default=750,
|
p.add_argument("--bus-speed", type=int, choices=[20, 100, 400, 750], default=750,
|
||||||
help="I2C bus speed in kHz. The default cycle count runs near "
|
help="I2C bus speed in kHz. The default cycle count runs near "
|
||||||
"534 Hz, where 100 kHz would spend 80%% of each period on "
|
"282 Hz, where 100 kHz would spend 42%% of each period on "
|
||||||
"the bus; 750 spends 11%% (default: %(default)s)")
|
"the bus; 750 spends 6%% (default: %(default)s)")
|
||||||
p.add_argument("--calibrate", type=float, default=1.0,
|
p.add_argument("--calibrate", type=float, default=1.0,
|
||||||
help="seconds of loss-free samples used to measure the true "
|
help="seconds of loss-free samples used to measure the true "
|
||||||
"measurement period before recording starts; the run "
|
"measurement period before recording starts; the run "
|
||||||
|
|
@ -233,13 +238,70 @@ def print_plan(cfg, bus_speed, requested_rate, sampler_nice=None):
|
||||||
print(f" priority {detail}")
|
print(f" priority {detail}")
|
||||||
for note in cfg.notes:
|
for note in cfg.notes:
|
||||||
print(f" note: {note}")
|
print(f" note: {note}")
|
||||||
# Bus speed is validated but never changed silently -- swapping it would
|
|
||||||
# hide a wiring problem behind a configuration change.
|
# Nothing below is ever fixed silently: changing a setting the user asked
|
||||||
|
# for would hide the problem behind a configuration change. Say what is
|
||||||
|
# wrong and what would fix it, then run what was requested.
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Below the recommended cycle count the LSB grows faster than the sensor's
|
||||||
|
# own noise, so the quantiser stops being dithered and averaging stalls.
|
||||||
|
# The manual's hard floor is 30 (section 5.1), but the margin is already
|
||||||
|
# slim at 50, so anything under that is worth saying out loud.
|
||||||
|
if cfg.cycle_count < rm3100.RECOMMENDED_MIN_CYCLE_COUNT:
|
||||||
|
warnings.append(
|
||||||
|
f"cycle count {cfg.cycle_count} is below the recommended "
|
||||||
|
f"{rm3100.RECOMMENDED_MIN_CYCLE_COUNT}: dither is {dither:.2f} LSB "
|
||||||
|
f"at spec noise ({lsb:.1f} nT/LSB) against the ~0.2 where averaging "
|
||||||
|
"stops recovering sub-LSB resolution. characterize.py prints sd/LSB "
|
||||||
|
"per axis, which settles it from the first capture.")
|
||||||
|
|
||||||
|
# The measured worst configuration mistake -- TMRC slower than the
|
||||||
|
# cycle-count ceiling leaves the sensor idle, and idle time buys nothing.
|
||||||
|
# Idle is the shortfall against the ceiling, which is what TMRC costs; it is
|
||||||
|
# not 1 - duty, since the fixed per-axis overhead is active time too.
|
||||||
|
idle = 1.0 - cfg.predicted_hz * rm3100.sample_period(cfg.cycle_count)
|
||||||
|
if cfg.governed_by == "TMRC" and idle > 0.2:
|
||||||
|
warnings.append(
|
||||||
|
f"TMRC 0x{cfg.tmrc:02x} governs and leaves the sensor idle "
|
||||||
|
f"{idle:.0%} of each period, holding duty to {cfg.duty:.0%}. "
|
||||||
|
"Measured cost at 23% duty was 1.43x the noise ASD. Raising the "
|
||||||
|
"cycle count instead reaches the same rate at ~100% duty.")
|
||||||
|
|
||||||
|
# A rate that came back different from the one asked for is easy to miss in
|
||||||
|
# a note, and every figure above is derived from the rate.
|
||||||
|
if (requested_rate is not None and abs(cfg.predicted_hz / requested_rate - 1)
|
||||||
|
> rm3100.RATE_TOLERANCE):
|
||||||
|
warnings.append(
|
||||||
|
f"{requested_rate:g} Hz was requested but this configuration runs "
|
||||||
|
f"at {cfg.predicted_hz:.2f} Hz.")
|
||||||
|
|
||||||
|
# Mains and its harmonics fold onto signal below 2x mains, and no amount of
|
||||||
|
# post-filtering undoes an alias.
|
||||||
|
if cfg.predicted_hz < 2 * MAINS_HZ:
|
||||||
|
alias = abs(MAINS_HZ - round(MAINS_HZ / cfg.predicted_hz) * cfg.predicted_hz)
|
||||||
|
warnings.append(
|
||||||
|
f"Nyquist is {cfg.predicted_hz / 2:.1f} Hz, below {MAINS_HZ:g} Hz "
|
||||||
|
f"mains: interference folds to {alias:.2f} Hz and cannot be removed "
|
||||||
|
"afterwards. Sampling faster and decimating gives the same noise "
|
||||||
|
"floor with the mains line still visible.")
|
||||||
|
|
||||||
|
# Table 3-1 stops at 400, so the noise figure printed above is extrapolation
|
||||||
|
# past that point rather than a specification.
|
||||||
|
if cfg.cycle_count > rm3100.MAX_SPEC_CYCLE_COUNT:
|
||||||
|
warnings.append(
|
||||||
|
f"cycle count {cfg.cycle_count:,} is past the {rm3100.MAX_SPEC_CYCLE_COUNT} "
|
||||||
|
"where Table 3-1 ends, so the spec noise and gain above are "
|
||||||
|
"extrapolated, not specified.")
|
||||||
|
|
||||||
if share > 0.5:
|
if share > 0.5:
|
||||||
faster = [s for s in sorted(BUS_SPEEDS) if i2c_bus_time(s) / period < 0.25]
|
faster = [s for s in sorted(BUS_SPEEDS) if i2c_bus_time(s) / period < 0.25]
|
||||||
fix = f"; {faster[0]} kHz would fit" if faster else ""
|
fix = f"; {faster[0]} kHz would fit" if faster else ""
|
||||||
print(f" WARNING: the bus needs {share:.0%} of every period{fix}",
|
warnings.append(f"the bus needs {share:.0%} of every period{fix}.")
|
||||||
file=sys.stderr)
|
|
||||||
|
for text in warnings:
|
||||||
|
print(textwrap.fill(text, width=79, initial_indent=" WARNING: ",
|
||||||
|
subsequent_indent=" "), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def find_sensor(bus, address):
|
def find_sensor(bus, address):
|
||||||
|
|
|
||||||
24
rm3100.py
24
rm3100.py
|
|
@ -46,18 +46,18 @@ STATUS_DRDY = 0x80
|
||||||
|
|
||||||
EXPECTED_REVID = 0x22
|
EXPECTED_REVID = 0x22
|
||||||
|
|
||||||
# 50 buys bandwidth almost for free. Against cycle count 200 it costs 4.4% in
|
# 100 cycle count gives a good balance of sensor duty cycle, spectrum, and post-filter noise floor.
|
||||||
# post-filter noise (89% duty against 97%) and nothing else, while giving 3.7x
|
# This should be raised if data rates are higher than desired or bus speed / host speed can't support.
|
||||||
# the spectrum -- 267 Hz of Nyquist against 73 Hz. That matters because aliased
|
# Lower than 50 is not recommended under any circumstances. See the README for more info.
|
||||||
# interference cannot be filtered out afterwards at any cycle count, so seeing
|
DEFAULT_CYCLE_COUNT = 100
|
||||||
# it is worth more than a few percent of noise.
|
|
||||||
#
|
# Not a hard floor -- the manual's is MIN_CYCLE_COUNT below -- but the point
|
||||||
# The one thing to verify rather than assume is dither: 50 sits at 0.58 LSB of
|
# under which the dither margin is too thin to defend, so logger.py warns.
|
||||||
# intrinsic noise, which simulation puts safely in the region where averaging
|
RECOMMENDED_MIN_CYCLE_COUNT = 50
|
||||||
# still recovers sub-LSB resolution (it fails below ~0.2). characterize.py
|
|
||||||
# prints sd/LSB, which answers it from the first capture. Fall back to 100 or
|
# Table 3-1 tabulates gain and noise up to a cycle count of 400. Past that both
|
||||||
# 200 if that comes back low.
|
# _gain_lsb_per_tesla and expected_noise_nt are extrapolating their fits.
|
||||||
DEFAULT_CYCLE_COUNT = 50
|
MAX_SPEC_CYCLE_COUNT = 400
|
||||||
|
|
||||||
# The fastest rate register. Used by default so the cycle count, not TMRC, sets
|
# The fastest rate register. Used by default so the cycle count, not TMRC, sets
|
||||||
# the rate -- TMRC offers only factor-of-two steps and has no effect at all once
|
# the rate -- TMRC offers only factor-of-two steps and has no effect at all once
|
||||||
|
|
|
||||||
83
setup.sh
83
setup.sh
|
|
@ -3,15 +3,14 @@
|
||||||
# One-time system setup for the CH347 USB-I2C adapter and the RM3100 logger.
|
# One-time system setup for the CH347 USB-I2C adapter and the RM3100 logger.
|
||||||
# Idempotent: safe to re-run.
|
# Idempotent: safe to re-run.
|
||||||
#
|
#
|
||||||
# Needs sudo for exactly one thing: installing the udev rule that grants the
|
# Needs sudo for exactly one thing: installing the udev rule that grants this
|
||||||
# plugdev group access to the adapter's USB node.
|
# user access to the adapter's USB node.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VID=1a86
|
VID=1a86
|
||||||
PID=55db
|
PID=55db
|
||||||
RULE_FILE=/etc/udev/rules.d/99-ch347.rules
|
RULE_FILE=/etc/udev/rules.d/99-ch347.rules
|
||||||
RULE='SUBSYSTEM=="usb", ATTRS{idVendor}=="'"$VID"'", ATTRS{idProduct}=="'"$PID"'", GROUP="plugdev", MODE="0660"'
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
|
@ -26,25 +25,48 @@ lsusb -d "$VID:$PID"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== 2. Installing udev rule =="
|
echo "== 2. Installing udev rule =="
|
||||||
|
# Access is granted two ways, because no single mechanism covers every distro:
|
||||||
|
#
|
||||||
|
# TAG+="uaccess" systemd-logind puts an ACL on the node for whoever is
|
||||||
|
# logged in at the local seat. Needs no group and no logout,
|
||||||
|
# and is the only thing that works unaided on atomic Fedora
|
||||||
|
# (Bazzite, Silverblue, Kinoite) -- see section 4.
|
||||||
|
# GROUP=/MODE= the traditional fallback, for ssh sessions and seatless
|
||||||
|
# systems where there is no local seat for uaccess to grant.
|
||||||
|
#
|
||||||
|
# The group is picked from those that exist. plugdev is a Debian convention and
|
||||||
|
# is absent on Fedora; naming a group that does not exist makes udev log an
|
||||||
|
# error and leave the node owned by root, which is worse than omitting it.
|
||||||
|
ACCESS_GROUP=""
|
||||||
|
for g in plugdev dialout; do
|
||||||
|
if getent group "$g" >/dev/null; then ACCESS_GROUP=$g; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
RULE="SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"$VID\", ATTRS{idProduct}==\"$PID\", TAG+=\"uaccess\""
|
||||||
|
if [[ -n "$ACCESS_GROUP" ]]; then
|
||||||
|
RULE="$RULE, GROUP=\"$ACCESS_GROUP\", MODE=\"0660\""
|
||||||
|
echo "Access via uaccess, falling back to group '$ACCESS_GROUP'."
|
||||||
|
else
|
||||||
|
echo "No plugdev or dialout group here; relying on uaccess alone."
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -f "$RULE_FILE" ]] && [[ "$(cat "$RULE_FILE")" == "$RULE" ]]; then
|
if [[ -f "$RULE_FILE" ]] && [[ "$(cat "$RULE_FILE")" == "$RULE" ]]; then
|
||||||
echo "$RULE_FILE already up to date, skipping."
|
echo "$RULE_FILE already up to date, skipping."
|
||||||
else
|
else
|
||||||
echo "Writing $RULE_FILE (needs sudo)..."
|
echo "Writing $RULE_FILE (needs sudo)..."
|
||||||
printf '%s\n' "$RULE" | sudo tee "$RULE_FILE" >/dev/null
|
printf '%s\n' "$RULE" | sudo tee "$RULE_FILE" >/dev/null
|
||||||
sudo udevadm control --reload-rules
|
sudo udevadm control --reload-rules
|
||||||
sudo udevadm trigger --subsystem-match=usb
|
# --action=add, not the default 'change': the uaccess builtin that sets the
|
||||||
# udevadm trigger returns before the rule has necessarily been applied.
|
# ACL only runs on add, so a change event would install the rule without
|
||||||
|
# granting anything until the next replug. Matched to this device alone so
|
||||||
|
# re-running setup does not re-add every USB device on the system.
|
||||||
|
sudo udevadm trigger --action=add --subsystem-match=usb \
|
||||||
|
--attr-match=idVendor="$VID" --attr-match=idProduct="$PID"
|
||||||
|
# trigger returns before the rule has necessarily been applied.
|
||||||
sudo udevadm settle
|
sudo udevadm settle
|
||||||
echo "Installed."
|
echo "Installed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! id -nG | tr ' ' '\n' | grep -qx plugdev; then
|
|
||||||
echo "WARNING: $(id -un) is not in the 'plugdev' group, so the rule above" >&2
|
|
||||||
echo " will not grant access. Fix with:" >&2
|
|
||||||
echo " sudo usermod -aG plugdev $(id -un)" >&2
|
|
||||||
echo " then log out and back in." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== 3. Creating virtualenv and installing pyusb =="
|
echo "== 3. Creating virtualenv and installing pyusb =="
|
||||||
if [[ ! -d .venv ]]; then
|
if [[ ! -d .venv ]]; then
|
||||||
|
|
@ -59,16 +81,45 @@ echo "pyusb $(./.venv/bin/python -c 'import usb; print(usb.__version__)') instal
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== 4. Verifying device node permissions =="
|
echo "== 4. Verifying device node permissions =="
|
||||||
# Resolve the bus/device path for this specific adapter.
|
# The node itself is the ground truth, so nothing above is warned about
|
||||||
|
# speculatively -- group membership only matters if uaccess did not already
|
||||||
|
# cover it, and that is visible here.
|
||||||
NODE=$(lsusb -d "$VID:$PID" | head -1 |
|
NODE=$(lsusb -d "$VID:$PID" | head -1 |
|
||||||
sed -E 's|Bus ([0-9]+) Device ([0-9]+).*|/dev/bus/usb/\1/\2|')
|
sed -E 's|Bus ([0-9]+) Device ([0-9]+).*|/dev/bus/usb/\1/\2|')
|
||||||
ls -l "$NODE"
|
ls -l "$NODE"
|
||||||
|
getfacl -p "$NODE" 2>/dev/null | grep -E "^user:[^:]+:" || true
|
||||||
|
|
||||||
if [[ -w "$NODE" ]]; then
|
if [[ -w "$NODE" ]]; then
|
||||||
echo "OK: $NODE is writable by $(id -un)."
|
echo "OK: $NODE is writable by $(id -un)."
|
||||||
else
|
else
|
||||||
echo "WARNING: $NODE is not writable by $(id -un)." >&2
|
USER_NAME=$(id -un)
|
||||||
echo " If the group above is already 'plugdev', unplug and replug" >&2
|
echo "WARNING: $NODE is not writable by $USER_NAME." >&2
|
||||||
echo " the adapter so the new rule is applied to a fresh node." >&2
|
echo >&2
|
||||||
|
echo " uaccess grants nothing over ssh or without a local seat, so fall" >&2
|
||||||
|
echo " back to the group. Try, in order:" >&2
|
||||||
|
echo >&2
|
||||||
|
echo " 1. Unplug and replug the adapter, so the rule applies to a fresh" >&2
|
||||||
|
echo " node. This is enough on a local desktop session." >&2
|
||||||
|
if [[ -n "$ACCESS_GROUP" ]] &&
|
||||||
|
! id -nG | tr ' ' '\n' | grep -qx "$ACCESS_GROUP"; then
|
||||||
|
echo >&2
|
||||||
|
if [[ -f /run/ostree-booted ]]; then
|
||||||
|
# usermod edits /etc/group directly, while getent also sees
|
||||||
|
# /usr/lib/group through nss_altfiles. On rpm-ostree systems the
|
||||||
|
# group is often only in the latter, so it looks present to every
|
||||||
|
# query and still fails to add: "group '$ACCESS_GROUP' does not
|
||||||
|
# exist". Copying the line across is what makes usermod agree.
|
||||||
|
echo " 2. This is an rpm-ostree system (Bazzite/Silverblue), where" >&2
|
||||||
|
echo " '$ACCESS_GROUP' may exist only in /usr/lib/group. usermod" >&2
|
||||||
|
echo " reads /etc/group alone and will refuse. Copy it over:" >&2
|
||||||
|
echo " grep -E '^$ACCESS_GROUP:' /usr/lib/group | sudo tee -a /etc/group" >&2
|
||||||
|
echo " then:" >&2
|
||||||
|
else
|
||||||
|
echo " 2. Add yourself to the group:" >&2
|
||||||
|
fi
|
||||||
|
echo " sudo usermod -aG $ACCESS_GROUP $USER_NAME" >&2
|
||||||
|
echo " and log out and back in for it to take effect." >&2
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue