add scripts
This commit is contained in:
parent
3be523891a
commit
e2a3d6e266
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
/.vscode
|
|
||||||
/target
|
|
||||||
/Cargo.lock
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "toes-matter"
|
name = "toes-matter"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
@ -19,7 +19,7 @@ env_logger = "0.11"
|
||||||
futures-lite = "2"
|
futures-lite = "2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = { version = "0.8", features = ["std", "std_rng"] }
|
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",
|
"log",
|
||||||
"os",
|
"os",
|
||||||
"rustcrypto",
|
"rustcrypto",
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -35,4 +35,23 @@ fn main() -> toes_matter::Result<()> {
|
||||||
|
|
||||||
## Development credentials
|
## 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
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
87
scripts/provision-device.sh
Executable 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"
|
||||||
17
src/bin/toes-matter-creds.rs
Normal file
17
src/bin/toes-matter-creds.rs
Normal 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))
|
||||||
|
}
|
||||||
97
src/lib.rs
97
src/lib.rs
|
|
@ -42,7 +42,9 @@ use rs_matter::pairing::qr::{no_optional_data, CommFlowType, QrPayload, QrTextTy
|
||||||
use rs_matter::pairing::DiscoveryCapabilities;
|
use rs_matter::pairing::DiscoveryCapabilities;
|
||||||
use rs_matter::persist::{DirKvBlobStore, SharedKvBlobStore};
|
use rs_matter::persist::{DirKvBlobStore, SharedKvBlobStore};
|
||||||
use rs_matter::respond::DefaultResponder;
|
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::tlv::Nullable;
|
||||||
use rs_matter::transport::network::btp::bluez;
|
use rs_matter::transport::network::btp::bluez;
|
||||||
use rs_matter::transport::network::btp::{AdvData, Btp};
|
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::blocking::Mutex;
|
||||||
use rs_matter::utils::sync::DynBase;
|
use rs_matter::utils::sync::DynBase;
|
||||||
use rs_matter::utils::zbus::Connection;
|
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>;
|
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"),
|
path.join("dac-private-key.raw"),
|
||||||
TEST_DEV_ATT.dac_priv_key().access(),
|
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(
|
let payload = QrPayload::new_from_basic_info(
|
||||||
DiscoveryCapabilities::BLE,
|
DiscoveryCapabilities::BLE,
|
||||||
CommFlowType::Standard,
|
CommFlowType::Standard,
|
||||||
TEST_DEV_COMM,
|
comm.clone(),
|
||||||
&TEST_DEV_DET,
|
&TEST_DEV_DET,
|
||||||
no_optional_data,
|
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 (qr_text, _) = payload.as_str(&mut qr_buf)?;
|
||||||
|
|
||||||
let setup = format!(
|
let setup = format!(
|
||||||
"setup_passcode=20202021\n\
|
"setup_passcode={}\n\
|
||||||
discriminator=3840\n\
|
discriminator={}\n\
|
||||||
manual_code={}\n\
|
manual_code={}\n\
|
||||||
qr_code={}\n",
|
qr_code={}\n",
|
||||||
TEST_DEV_COMM.compute_pretty_pairing_code(),
|
comm_passcode(&comm),
|
||||||
|
comm.discriminator,
|
||||||
|
comm.compute_pretty_pairing_code(),
|
||||||
qr_text,
|
qr_text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -166,6 +172,77 @@ pub async fn generate_credentials(path: impl AsRef<Path>) -> Result<()> {
|
||||||
Ok(())
|
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.
|
/// 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.
|
/// 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
|
where
|
||||||
N: NetCtl + WifiDiag + NetChangeNotif,
|
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 networks = WifiNetworks::<3>::new();
|
||||||
let mut events: Events = Events::new_default();
|
let mut events: Events = Events::new_default();
|
||||||
let mut kv_buf = [0_u8; KV_BUF_SIZE];
|
let mut kv_buf = [0_u8; KV_BUF_SIZE];
|
||||||
|
|
@ -281,7 +359,7 @@ where
|
||||||
let btp = Btp::new();
|
let btp = Btp::new();
|
||||||
btp.set_relaxed_mtu_nego(true);
|
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(
|
let mut bluetooth = pin!(bluez::run_peripheral(
|
||||||
connection, None, "TOES", &adv_data, &btp
|
connection, None, "TOES", &adv_data, &btp
|
||||||
));
|
));
|
||||||
|
|
@ -311,7 +389,8 @@ async fn run_listen<N>(net_ctl: N, config: &Config) -> Result<()>
|
||||||
where
|
where
|
||||||
N: NetCtl + WifiDiag + NetChangeNotif,
|
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 networks = WifiNetworks::<3>::new();
|
||||||
let mut events: Events = Events::new_default();
|
let mut events: Events = Events::new_default();
|
||||||
let mut kv_buf = [0_u8; KV_BUF_SIZE];
|
let mut kv_buf = [0_u8; KV_BUF_SIZE];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user