159 lines
4.8 KiB
Bash
Executable File
159 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# matter-decode.sh — decode a Matter commissioning QR code (MT: URI) from the CLI.
|
|
#
|
|
# This is a bash port of the decode logic in matter-qr-decoder.html (the browser
|
|
# app's camera/QR-scanning feature has no shell equivalent and is intentionally
|
|
# omitted). It decodes the 84-bit payload and prints the same fields the web UI shows.
|
|
#
|
|
# Usage:
|
|
# ./matter-decode.sh "MT:..."
|
|
# echo "MT:..." | ./matter-decode.sh
|
|
#
|
|
# A companion file manufacturers.tsv (same directory) maps vendor IDs to names.
|
|
# It is derived from project-chip/connectedhomeip's manufacturers.xml and is
|
|
# optional: without it, all fields still decode — the vendor name line is simply
|
|
# skipped. Regenerate it with: python3 gen-manufacturers.py (see that file).
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
|
|
|
|
# Base38 charset, per the Matter spec: 0-9, A-Z, '-', '.' (38 symbols).
|
|
CHARS=( 0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - . )
|
|
declare -A IDX # char -> position
|
|
for (( _i=0; _i<${#CHARS[@]}; _i++ )); do IDX["${CHARS[_i]}"]=$_i; done
|
|
# Powers of 38 for the 5-char (3-byte) chunk; 38^0..38^4.
|
|
POW38=( 1 38 1444 54872 2085136 )
|
|
|
|
BYTES=() # decoded payload as a byte array, little-endian
|
|
|
|
# base38_decode STRING -> fills global BYTES.
|
|
# Greedy chunking: 5 chars -> 3 bytes, 4 -> 2, 2 -> 1, matching the reference.
|
|
|
|
base38_decode() {
|
|
local s=$1
|
|
local i=0 len=${#s} j value char idx chunkSize byteCount
|
|
BYTES=()
|
|
while (( i < len )); do
|
|
if (( i + 5 <= len )); then chunkSize=5; byteCount=3
|
|
elif (( i + 4 <= len )); then chunkSize=4; byteCount=2
|
|
elif (( i + 2 <= len )); then chunkSize=2; byteCount=1
|
|
else
|
|
echo "Error: Invalid base38 string length" >&2
|
|
return 1
|
|
fi
|
|
|
|
value=0
|
|
|
|
for (( j=0; j<chunkSize; j++ ))
|
|
do
|
|
char=${s:i+j:1}
|
|
idx=${IDX[$char]:-_} # resolve in param-expansion context;
|
|
if [[ $idx == _ ]]; then # avoids an operator-char array key inside $(( ))
|
|
echo "Error: Invalid character: $char" >&2
|
|
return 1
|
|
fi
|
|
value=$(( value + idx * POW38[j] ))
|
|
done
|
|
|
|
for (( j=0; j<byteCount; j++ ))
|
|
do
|
|
BYTES+=( $(( (value >> (8*j)) & 0xFF )) )
|
|
done
|
|
|
|
i=$(( i + chunkSize ))
|
|
|
|
done
|
|
}
|
|
|
|
# extract_bits START COUNT -> prints COUNT bits of BYTES, LSB-first, as a number.
|
|
|
|
extract_bits() {
|
|
local start=$1 count=$2 i result=0 bitIndex byteIndex bitOffset bit
|
|
for (( i=0; i<count; i++ )); do
|
|
bitIndex=$(( start + i ))
|
|
byteIndex=$(( bitIndex / 8 ))
|
|
bitOffset=$(( bitIndex % 8 ))
|
|
|
|
if (( byteIndex < ${#BYTES[@]} ))
|
|
then
|
|
bit=$(( (BYTES[byteIndex] >> bitOffset) & 1 ))
|
|
result=$(( result | (bit << i) ))
|
|
fi
|
|
done
|
|
|
|
printf '%d' "$result"
|
|
}
|
|
|
|
# get_vendor_name DECID -> prints name if in manufacturers.tsv, else nothing.
|
|
|
|
get_vendor_name() {
|
|
local tsv="$SCRIPT_DIR/manufacturers.tsv"
|
|
[[ -f $tsv ]] || return 0
|
|
awk -F'\t' -v id="$1" '$1==id { print $2; exit }' "$tsv"
|
|
}
|
|
|
|
main() {
|
|
local uri payload
|
|
|
|
uri=${1:-}
|
|
|
|
[[ -z $uri ]] && uri=$(cat)
|
|
|
|
# Trim leading/trailing whitespace
|
|
|
|
uri="${uri#"${uri%%[![:space:]]*}"}"
|
|
uri="${uri%"${uri##*[![:space:]]}"}"
|
|
|
|
if [[ $uri != MT:* ]]; then
|
|
echo 'Error: URI must start with "MT:"' >&2
|
|
return 1
|
|
fi
|
|
|
|
payload=${uri#MT:}
|
|
|
|
[[ -n $payload ]] || { echo 'Error: No data found after "MT:"' >&2; return 1; }
|
|
|
|
base38_decode "$payload" || return 1
|
|
|
|
local version vendor product customFlow discovery discriminator passcode
|
|
local cf caps="" vname
|
|
|
|
version=$(extract_bits 0 3)
|
|
vendor=$(extract_bits 3 16)
|
|
product=$(extract_bits 19 16)
|
|
customFlow=$(extract_bits 35 2)
|
|
discovery=$(extract_bits 37 8)
|
|
discriminator=$(extract_bits 45 12)
|
|
passcode=$(extract_bits 57 27)
|
|
|
|
case $customFlow in
|
|
0) cf="Standard Commissioning Flow" ;;
|
|
1) cf="User Intent Commissioning Flow" ;;
|
|
2) cf="Custom Commissioning Flow" ;;
|
|
3) cf="Reserved" ;;
|
|
*) cf="Unknown ($customFlow)" ;;
|
|
esac
|
|
|
|
(( discovery & 0x01 )) && caps="SoftAP"
|
|
(( discovery & 0x02 )) && caps="${caps:+$caps, }BLE"
|
|
(( discovery & 0x04 )) && caps="${caps:+$caps, }On IP Network"
|
|
|
|
[[ -z $caps ]] && caps="None"
|
|
|
|
vname=$(get_vendor_name "$vendor")
|
|
|
|
printf 'Version: %s\n' "$version"
|
|
printf 'Vendor ID: %s (0x%04X)' "$vendor" "$vendor"
|
|
[[ -n $vname ]] && printf ' %s' "$vname"
|
|
printf '\n'
|
|
printf 'Product ID: %s (0x%04X)\n' "$product" "$product"
|
|
printf 'Custom Flow: %s\n' "$cf"
|
|
printf 'Discovery: %s\n' "$caps"
|
|
printf 'Discriminator: %s\n' "$discriminator"
|
|
printf 'Passcode: %s\n' "$passcode"
|
|
}
|
|
|
|
main "$@"
|