add scripts

This commit is contained in:
Pat Nakajima 2026-05-17 22:01:34 -07:00
parent 3be523891a
commit e2a3d6e266
8 changed files with 214 additions and 51 deletions

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
/.vscode
/target
/Cargo.lock

View File

@ -1,6 +1,6 @@
[package]
name = "toes-matter"
version = "0.1.1"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
readme = "README.md"
@ -19,7 +19,7 @@ env_logger = "0.11"
futures-lite = "2"
log = "0.4"
rand = { version = "0.8", features = ["std", "std_rng"] }
rs-matter = { git = "https://git.fishmt.net/nakajima/rs-matter", branch = "bluez-ios-reconnect", default-features = false, features = [
rs-matter = { path = "../../rs-matter/rs-matter", default-features = false, features = [
"log",
"os",
"rustcrypto",

View File

@ -35,4 +35,23 @@ fn main() -> toes_matter::Result<()> {
## Development credentials
`generate_credentials()` writes rs-matter's built-in development/test DAC/PAI/CD plus setup QR/manual-code metadata. These are for local development, not production.
`generate_credentials()` writes rs-matter's built-in development/test DAC/PAI/CD plus setup QR/manual-code metadata. It creates a random setup passcode/discriminator the first time and reuses an existing `setup.txt` on later runs. These credentials are for local development, not production.
## Post-flash headless provisioning
After flashing a Linux device reachable over SSH:
```bash
toes-matter/scripts/provision-device.sh user@device-host device-001
```
The script generates `manufacturing/device-001/creds`, prints the manual code / QR payload, renders a terminal QR if `qrencode` is installed, copies the creds to `/var/lib/toes-matter/creds`, and restarts `toes-matter.service` if present.
Set these if your service uses different paths:
```bash
REMOTE_CREDS_DIR=/var/lib/my-app/creds \
REMOTE_STATE_DIR=/var/lib/my-app/state \
SERVICE=my-app.service \
toes-matter/scripts/provision-device.sh user@device-host device-001
```

View File

@ -1,36 +0,0 @@
[project]
name = "toes-matter"
auto-tag = true # detect Cargo.toml version, create/push git tag v<version> before releasing to git
# binary = "toes-matter" # defaults to project name
# package = "toes-matter" # optional workspace package override; auto-detected from binary when unique
# binaries = ["toes-matter", "toes-matter-cli"] # optional extra release assets
repo = "nakajima/toes-matter"
# version-command = "git describe --tags --abbrev=0"
[build]
command = "cargo build --release --target {target}"
artifact = "target/{target}/release/{binary}"
targets = [
"aarch64-unknown-linux-gnu",
]
[git]
type = "gitea" # defaults to "github"
base-url = "https://git.fishmt.net"
# api-base-url = "https://git.example.com/api/v1" # defaults from type/base-url
# token-env = "GITEA_TOKEN" # defaults: GITHUB_TOKEN or GITEA_TOKEN
[channels.git]
enabled = true
# [channels.homebrew]
# tap = "owner/homebrew-tap"
# formula-name = "toes-matter"
# [channels.cargo]
# crate-name = "toes-matter"
[channels.curl]
# [channels.nix]
# flake-repo = "owner/nix-repo" # defaults to project repo

87
scripts/provision-device.sh Executable file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
# Post-flash headless provisioning helper.
#
# Generates the Matter setup payload on this host, prints the QR/manual code,
# and copies the generated creds/config directory to a freshly flashed Linux device.
#
# Usage:
# toes-matter/scripts/provision-device.sh user@device-host [device-id]
#
# Environment:
# CREDS_DIR Local output dir. Default: toes-matter/manufacturing/<device-id>/creds
# REMOTE_CREDS_DIR Remote creds dir. Default: /var/lib/toes-matter/creds
# REMOTE_STATE_DIR Remote Matter state dir. Default: /var/lib/toes-matter/state
# SERVICE Optional service to restart. Default: toes-matter.service
# Set SERVICE= to skip restart.
# SSH_OPTS Extra ssh/scp options, e.g. '-p 2222'
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: $0 user@device-host [device-id]" >&2
exit 2
fi
TARGET="$1"
DEVICE_ID="${2:-$(date +%Y%m%d-%H%M%S)}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CREDS_DIR="${CREDS_DIR:-$CRATE_DIR/manufacturing/$DEVICE_ID/creds}"
REMOTE_CREDS_DIR="${REMOTE_CREDS_DIR:-/var/lib/toes-matter/creds}"
REMOTE_STATE_DIR="${REMOTE_STATE_DIR:-/var/lib/toes-matter/state}"
SERVICE="${SERVICE-toes-matter.service}"
SSH_OPTS="${SSH_OPTS:-}"
REMOTE_TMP="/tmp/toes-matter-creds-$DEVICE_ID-$$"
# shellcheck disable=SC2206
SSH_ARGS=($SSH_OPTS)
mkdir -p "$CREDS_DIR"
echo "==> Generating development Matter credentials in $CREDS_DIR"
cargo run --quiet --manifest-path "$CRATE_DIR/Cargo.toml" --bin toes-matter-creds -- "$CREDS_DIR"
SETUP_FILE="$CREDS_DIR/setup.txt"
MANUAL_CODE="$(awk -F= '$1 == "manual_code" { print $2 }' "$SETUP_FILE")"
QR_CODE="$(awk -F= '$1 == "qr_code" { print $2 }' "$SETUP_FILE")"
echo
echo "==> Pairing info for device $DEVICE_ID"
echo "Manual code: $MANUAL_CODE"
echo "QR payload : $QR_CODE"
if command -v qrencode >/dev/null 2>&1; then
echo
echo "==> QR code"
qrencode -t ANSIUTF8 "$QR_CODE"
else
echo
echo "Tip: install qrencode to render the QR in this terminal: sudo apt install qrencode"
fi
echo
echo "==> Copying creds to $TARGET:$REMOTE_CREDS_DIR"
ssh "${SSH_ARGS[@]}" "$TARGET" "rm -rf '$REMOTE_TMP' && mkdir -p '$REMOTE_TMP'"
scp "${SSH_ARGS[@]}" -r "$CREDS_DIR"/. "$TARGET:$REMOTE_TMP/"
ssh "${SSH_ARGS[@]}" "$TARGET" "set -e
sudo rm -rf '$REMOTE_CREDS_DIR'
sudo mkdir -p '$(dirname "$REMOTE_CREDS_DIR")' '$REMOTE_STATE_DIR'
sudo mv '$REMOTE_TMP' '$REMOTE_CREDS_DIR'
sudo chmod -R go-rwx '$REMOTE_CREDS_DIR' '$REMOTE_STATE_DIR'
if [ -n '$SERVICE' ]; then
if systemctl list-unit-files '$SERVICE' >/dev/null 2>&1 || systemctl status '$SERVICE' >/dev/null 2>&1; then
sudo systemctl restart '$SERVICE'
else
echo 'Service $SERVICE not found; skipping restart'
fi
fi
"
echo
echo "==> Done"
echo "Local copy saved at: $CREDS_DIR"
echo "Remote app env should use:"
echo " TOES_MATTER_CREDS_DIR=$REMOTE_CREDS_DIR"
echo " TOES_MATTER_STATE_DIR=$REMOTE_STATE_DIR"

View File

@ -0,0 +1,17 @@
#![recursion_limit = "256"]
fn main() -> toes_matter::Result<()> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
let mut args = std::env::args_os().skip(1);
let dir = args.next().unwrap_or_else(|| "./creds".into());
if args.next().is_some() {
eprintln!("Usage: toes-matter-creds [CREDS_DIR]");
std::process::exit(2);
}
futures_lite::future::block_on(toes_matter::generate_credentials(dir))
}

View File

@ -42,7 +42,9 @@ use rs_matter::pairing::qr::{no_optional_data, CommFlowType, QrPayload, QrTextTy
use rs_matter::pairing::DiscoveryCapabilities;
use rs_matter::persist::{DirKvBlobStore, SharedKvBlobStore};
use rs_matter::respond::DefaultResponder;
use rs_matter::sc::pase::MAX_COMM_WINDOW_TIMEOUT_SECS;
use rs_matter::sc::pase::{
Spake2pVerifierPassword, Spake2pVerifierPasswordRef, MAX_COMM_WINDOW_TIMEOUT_SECS,
};
use rs_matter::tlv::Nullable;
use rs_matter::transport::network::btp::bluez;
use rs_matter::transport::network::btp::{AdvData, Btp};
@ -56,7 +58,7 @@ use rs_matter::utils::storage::pooled::PooledBuffers;
use rs_matter::utils::sync::blocking::Mutex;
use rs_matter::utils::sync::DynBase;
use rs_matter::utils::zbus::Connection;
use rs_matter::{clusters, devices, root_endpoint, with, Matter, MATTER_PORT};
use rs_matter::{clusters, devices, root_endpoint, with, BasicCommData, Matter, MATTER_PORT};
pub type Result<T> = core::result::Result<T, Error>;
@ -141,10 +143,12 @@ pub async fn generate_credentials(path: impl AsRef<Path>) -> Result<()> {
path.join("dac-private-key.raw"),
TEST_DEV_ATT.dac_priv_key().access(),
)?;
let comm = read_comm_data(path)?.unwrap_or_else(random_comm_data);
let payload = QrPayload::new_from_basic_info(
DiscoveryCapabilities::BLE,
CommFlowType::Standard,
TEST_DEV_COMM,
comm.clone(),
&TEST_DEV_DET,
no_optional_data,
);
@ -153,11 +157,13 @@ pub async fn generate_credentials(path: impl AsRef<Path>) -> Result<()> {
let (qr_text, _) = payload.as_str(&mut qr_buf)?;
let setup = format!(
"setup_passcode=20202021\n\
discriminator=3840\n\
"setup_passcode={}\n\
discriminator={}\n\
manual_code={}\n\
qr_code={}\n",
TEST_DEV_COMM.compute_pretty_pairing_code(),
comm_passcode(&comm),
comm.discriminator,
comm.compute_pretty_pairing_code(),
qr_text,
);
@ -166,6 +172,77 @@ pub async fn generate_credentials(path: impl AsRef<Path>) -> Result<()> {
Ok(())
}
fn load_comm_data(config: &Config) -> Result<BasicCommData> {
match read_comm_data(&config.creds_dir)? {
Some(comm) => Ok(comm),
None => Ok(TEST_DEV_COMM.clone()),
}
}
fn read_comm_data(path: &Path) -> Result<Option<BasicCommData>> {
let setup_path = path.join("setup.txt");
let setup = match std::fs::read_to_string(setup_path) {
Ok(setup) => setup,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err.into()),
};
let passcode = parse_setup_u32(&setup, "setup_passcode")?;
let discriminator = parse_setup_u32(&setup, "discriminator")?;
if discriminator > 0x0fff || !valid_setup_passcode(passcode) {
return Err(ErrorCode::InvalidData.into());
}
Ok(Some(make_comm_data(passcode, discriminator as u16)))
}
fn parse_setup_u32(setup: &str, key: &str) -> Result<u32> {
let value = setup
.lines()
.find_map(|line| line.strip_prefix(key)?.strip_prefix('='))
.ok_or(ErrorCode::InvalidData)?;
value.parse().map_err(|_| ErrorCode::InvalidData.into())
}
fn random_comm_data() -> BasicCommData {
let mut rng = rand::thread_rng();
let passcode = loop {
let passcode = (rng.next_u32() % 99_999_998) + 1;
if valid_setup_passcode(passcode) {
break passcode;
}
};
let discriminator = (rng.next_u32() & 0x0fff) as u16;
make_comm_data(passcode, discriminator)
}
fn make_comm_data(passcode: u32, discriminator: u16) -> BasicCommData {
BasicCommData {
password: Spake2pVerifierPassword::new_from_ref(Spake2pVerifierPasswordRef::new(
&passcode.to_le_bytes(),
)),
discriminator,
}
}
fn comm_passcode(comm: &BasicCommData) -> u32 {
u32::from_le_bytes(*comm.password.access())
}
fn valid_setup_passcode(passcode: u32) -> bool {
const INVALID: &[u32] = &[
0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888,
99999999, 12345678, 87654321,
];
(1..=99_999_998).contains(&passcode) && !INVALID.contains(&passcode)
}
/// Provision this device over BLE Matter commissioning and Wi-Fi Network Commissioning.
///
/// Returns immediately if a fabric is already stored in the configured state dir.
@ -218,7 +295,8 @@ async fn run_provision<N>(connection: &Connection, net_ctl: N, config: &Config)
where
N: NetCtl + WifiDiag + NetChangeNotif,
{
let mut matter = Matter::new_default(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, MATTER_PORT);
let comm = load_comm_data(config)?;
let mut matter = Matter::new_default(&TEST_DEV_DET, comm.clone(), &TEST_DEV_ATT, MATTER_PORT);
let mut networks = WifiNetworks::<3>::new();
let mut events: Events = Events::new_default();
let mut kv_buf = [0_u8; KV_BUF_SIZE];
@ -281,7 +359,7 @@ where
let btp = Btp::new();
btp.set_relaxed_mtu_nego(true);
let adv_data = AdvData::new(&TEST_DEV_DET, TEST_DEV_COMM.discriminator);
let adv_data = AdvData::new(&TEST_DEV_DET, comm.discriminator);
let mut bluetooth = pin!(bluez::run_peripheral(
connection, None, "TOES", &adv_data, &btp
));
@ -311,7 +389,8 @@ async fn run_listen<N>(net_ctl: N, config: &Config) -> Result<()>
where
N: NetCtl + WifiDiag + NetChangeNotif,
{
let mut matter = Matter::new_default(&TEST_DEV_DET, TEST_DEV_COMM, &TEST_DEV_ATT, MATTER_PORT);
let comm = load_comm_data(config)?;
let mut matter = Matter::new_default(&TEST_DEV_DET, comm, &TEST_DEV_ATT, MATTER_PORT);
let mut networks = WifiNetworks::<3>::new();
let mut events: Events = Events::new_default();
let mut kv_buf = [0_u8; KV_BUF_SIZE];