Initial commit.
This commit is contained in:
commit
548f2e701f
7 files changed
+2978
No files matched your search
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/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 "$@"
|
||||
Reference in new issue
Block a user