1*e7bb4389SLuca Ceresoli#!/bin/sh 2*e7bb4389SLuca Ceresoli# SPDX-License-Identifier: GPL-2.0 3*e7bb4389SLuca Ceresoli# 4*e7bb4389SLuca Ceresoli# Generate a graph of the current DAPM state for an audio card 5*e7bb4389SLuca Ceresoli# 6*e7bb4389SLuca Ceresoli# Copyright 2024 Bootlin 7*e7bb4389SLuca Ceresoli# Author: Luca Ceresoli <luca.ceresol@bootlin.com> 8*e7bb4389SLuca Ceresoli 9*e7bb4389SLuca Ceresoliset -eu 10*e7bb4389SLuca Ceresoli 11*e7bb4389SLuca CeresoliSTYLE_NODE_ON="shape=box,style=bold,color=green4" 12*e7bb4389SLuca CeresoliSTYLE_NODE_OFF="shape=box,style=filled,color=gray30,fillcolor=gray95" 13*e7bb4389SLuca Ceresoli 14*e7bb4389SLuca Ceresoli# Print usage and exit 15*e7bb4389SLuca Ceresoli# 16*e7bb4389SLuca Ceresoli# $1 = exit return value 17*e7bb4389SLuca Ceresoli# $2 = error string (required if $1 != 0) 18*e7bb4389SLuca Ceresoliusage() 19*e7bb4389SLuca Ceresoli{ 20*e7bb4389SLuca Ceresoli if [ "${1}" -ne 0 ]; then 21*e7bb4389SLuca Ceresoli echo "${2}" >&2 22*e7bb4389SLuca Ceresoli fi 23*e7bb4389SLuca Ceresoli 24*e7bb4389SLuca Ceresoli echo " 25*e7bb4389SLuca CeresoliGenerate a graph of the current DAPM state for an audio card. 26*e7bb4389SLuca Ceresoli 27*e7bb4389SLuca CeresoliThe DAPM state can be obtained via debugfs for a card on the local host or 28*e7bb4389SLuca Ceresolia remote target, or from a local copy of the debugfs tree for the card. 29*e7bb4389SLuca Ceresoli 30*e7bb4389SLuca CeresoliUsage: 31*e7bb4389SLuca Ceresoli $(basename $0) [options] -c CARD - Local sound card 32*e7bb4389SLuca Ceresoli $(basename $0) [options] -c CARD -r REMOTE_TARGET - Card on remote system 33*e7bb4389SLuca Ceresoli $(basename $0) [options] -d STATE_DIR - Local directory 34*e7bb4389SLuca Ceresoli 35*e7bb4389SLuca CeresoliOptions: 36*e7bb4389SLuca Ceresoli -c CARD Sound card to get DAPM state of 37*e7bb4389SLuca Ceresoli -r REMOTE_TARGET Get DAPM state from REMOTE_TARGET via SSH and SCP 38*e7bb4389SLuca Ceresoli instead of using a local sound card 39*e7bb4389SLuca Ceresoli -d STATE_DIR Get DAPM state from a local copy of a debugfs tree 40*e7bb4389SLuca Ceresoli -o OUT_FILE Output file (default: dapm.dot) 41*e7bb4389SLuca Ceresoli -D Show verbose debugging info 42*e7bb4389SLuca Ceresoli -h Print this help and exit 43*e7bb4389SLuca Ceresoli 44*e7bb4389SLuca CeresoliThe output format is implied by the extension of OUT_FILE: 45*e7bb4389SLuca Ceresoli 46*e7bb4389SLuca Ceresoli * Use the .dot extension to generate a text graph representation in 47*e7bb4389SLuca Ceresoli graphviz dot syntax. 48*e7bb4389SLuca Ceresoli * Any other extension is assumed to be a format supported by graphviz for 49*e7bb4389SLuca Ceresoli rendering, e.g. 'png', 'svg', and will produce both the .dot file and a 50*e7bb4389SLuca Ceresoli picture from it. This requires the 'dot' program from the graphviz 51*e7bb4389SLuca Ceresoli package. 52*e7bb4389SLuca Ceresoli" 53*e7bb4389SLuca Ceresoli 54*e7bb4389SLuca Ceresoli exit ${1} 55*e7bb4389SLuca Ceresoli} 56*e7bb4389SLuca Ceresoli 57*e7bb4389SLuca Ceresoli# Connect to a remote target via SSH, collect all DAPM files from debufs 58*e7bb4389SLuca Ceresoli# into a tarball and get the tarball via SCP into $3/dapm.tar 59*e7bb4389SLuca Ceresoli# 60*e7bb4389SLuca Ceresoli# $1 = target as used by ssh and scp, e.g. "root@192.168.1.1" 61*e7bb4389SLuca Ceresoli# $2 = sound card name 62*e7bb4389SLuca Ceresoli# $3 = temp dir path (present on the host, created on the target) 63*e7bb4389SLuca Ceresoli# $4 = local directory to extract the tarball into 64*e7bb4389SLuca Ceresoli# 65*e7bb4389SLuca Ceresoli# Requires an ssh+scp server, find and tar+gz on the target 66*e7bb4389SLuca Ceresoli# 67*e7bb4389SLuca Ceresoli# Note: the tarball is needed because plain 'scp -r' from debugfs would 68*e7bb4389SLuca Ceresoli# copy only empty files 69*e7bb4389SLuca Ceresoligrab_remote_files() 70*e7bb4389SLuca Ceresoli{ 71*e7bb4389SLuca Ceresoli echo "Collecting DAPM state from ${1}" 72*e7bb4389SLuca Ceresoli dbg_echo "Collected DAPM state in ${3}" 73*e7bb4389SLuca Ceresoli 74*e7bb4389SLuca Ceresoli ssh "${1}" " 75*e7bb4389SLuca Ceresoliset -eu && 76*e7bb4389SLuca Ceresolicd \"/sys/kernel/debug/asoc/${2}\" && 77*e7bb4389SLuca Ceresolifind * -type d -exec mkdir -p ${3}/dapm-tree/{} \; && 78*e7bb4389SLuca Ceresolifind * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; && 79*e7bb4389SLuca Ceresolicd ${3}/dapm-tree && 80*e7bb4389SLuca Ceresolitar cf ${3}/dapm.tar ." 81*e7bb4389SLuca Ceresoli scp -q "${1}:${3}/dapm.tar" "${3}" 82*e7bb4389SLuca Ceresoli 83*e7bb4389SLuca Ceresoli mkdir -p "${4}" 84*e7bb4389SLuca Ceresoli tar xf "${tmp_dir}/dapm.tar" -C "${4}" 85*e7bb4389SLuca Ceresoli} 86*e7bb4389SLuca Ceresoli 87*e7bb4389SLuca Ceresoli# Parse a widget file and generate graph description in graphviz dot format 88*e7bb4389SLuca Ceresoli# 89*e7bb4389SLuca Ceresoli# Skips any file named "bias_level". 90*e7bb4389SLuca Ceresoli# 91*e7bb4389SLuca Ceresoli# $1 = temporary work dir 92*e7bb4389SLuca Ceresoli# $2 = component name 93*e7bb4389SLuca Ceresoli# $3 = widget filename 94*e7bb4389SLuca Ceresoliprocess_dapm_widget() 95*e7bb4389SLuca Ceresoli{ 96*e7bb4389SLuca Ceresoli local tmp_dir="${1}" 97*e7bb4389SLuca Ceresoli local c_name="${2}" 98*e7bb4389SLuca Ceresoli local w_file="${3}" 99*e7bb4389SLuca Ceresoli local dot_file="${tmp_dir}/main.dot" 100*e7bb4389SLuca Ceresoli local links_file="${tmp_dir}/links.dot" 101*e7bb4389SLuca Ceresoli 102*e7bb4389SLuca Ceresoli local w_name="$(basename "${w_file}")" 103*e7bb4389SLuca Ceresoli local w_tag="${c_name}_${w_name}" 104*e7bb4389SLuca Ceresoli 105*e7bb4389SLuca Ceresoli if [ "${w_name}" = "bias_level" ]; then 106*e7bb4389SLuca Ceresoli return 0 107*e7bb4389SLuca Ceresoli fi 108*e7bb4389SLuca Ceresoli 109*e7bb4389SLuca Ceresoli dbg_echo " + Widget: ${w_name}" 110*e7bb4389SLuca Ceresoli 111*e7bb4389SLuca Ceresoli cat "${w_file}" | ( 112*e7bb4389SLuca Ceresoli read line 113*e7bb4389SLuca Ceresoli 114*e7bb4389SLuca Ceresoli if echo "${line}" | grep -q ': On ' 115*e7bb4389SLuca Ceresoli then local node_style="${STYLE_NODE_ON}" 116*e7bb4389SLuca Ceresoli else local node_style="${STYLE_NODE_OFF}" 117*e7bb4389SLuca Ceresoli fi 118*e7bb4389SLuca Ceresoli 119*e7bb4389SLuca Ceresoli local w_type="" 120*e7bb4389SLuca Ceresoli while read line; do 121*e7bb4389SLuca Ceresoli # Collect widget type if present 122*e7bb4389SLuca Ceresoli if echo "${line}" | grep -q '^widget-type '; then 123*e7bb4389SLuca Ceresoli local w_type_raw="$(echo "$line" | cut -d ' ' -f 2)" 124*e7bb4389SLuca Ceresoli dbg_echo " - Widget type: ${w_type_raw}" 125*e7bb4389SLuca Ceresoli 126*e7bb4389SLuca Ceresoli # Note: escaping '\n' is tricky to get working with both 127*e7bb4389SLuca Ceresoli # bash and busybox ash, so use a '%' here and replace it 128*e7bb4389SLuca Ceresoli # later 129*e7bb4389SLuca Ceresoli local w_type="%n[${w_type_raw}]" 130*e7bb4389SLuca Ceresoli fi 131*e7bb4389SLuca Ceresoli 132*e7bb4389SLuca Ceresoli # Collect any links. We could use "in" links or "out" links, 133*e7bb4389SLuca Ceresoli # let's use "in" links 134*e7bb4389SLuca Ceresoli if echo "${line}" | grep -q '^in '; then 135*e7bb4389SLuca Ceresoli local w_src=$(echo "$line" | 136*e7bb4389SLuca Ceresoli awk -F\" '{print $6 "_" $4}' | 137*e7bb4389SLuca Ceresoli sed 's/^(null)_/ROOT_/') 138*e7bb4389SLuca Ceresoli dbg_echo " - Input route from: ${w_src}" 139*e7bb4389SLuca Ceresoli echo " \"${w_src}\" -> \"$w_tag\"" >> "${links_file}" 140*e7bb4389SLuca Ceresoli fi 141*e7bb4389SLuca Ceresoli done 142*e7bb4389SLuca Ceresoli 143*e7bb4389SLuca Ceresoli echo " \"${w_tag}\" [label=\"${w_name}${w_type}\",${node_style}]" | 144*e7bb4389SLuca Ceresoli tr '%' '\\' >> "${dot_file}" 145*e7bb4389SLuca Ceresoli ) 146*e7bb4389SLuca Ceresoli} 147*e7bb4389SLuca Ceresoli 148*e7bb4389SLuca Ceresoli# Parse the DAPM tree for a sound card component and generate graph 149*e7bb4389SLuca Ceresoli# description in graphviz dot format 150*e7bb4389SLuca Ceresoli# 151*e7bb4389SLuca Ceresoli# $1 = temporary work dir 152*e7bb4389SLuca Ceresoli# $2 = component directory 153*e7bb4389SLuca Ceresoli# $3 = forced component name (extracted for path if empty) 154*e7bb4389SLuca Ceresoliprocess_dapm_component() 155*e7bb4389SLuca Ceresoli{ 156*e7bb4389SLuca Ceresoli local tmp_dir="${1}" 157*e7bb4389SLuca Ceresoli local c_dir="${2}" 158*e7bb4389SLuca Ceresoli local c_name="${3}" 159*e7bb4389SLuca Ceresoli local dot_file="${tmp_dir}/main.dot" 160*e7bb4389SLuca Ceresoli local links_file="${tmp_dir}/links.dot" 161*e7bb4389SLuca Ceresoli 162*e7bb4389SLuca Ceresoli if [ -z "${c_name}" ]; then 163*e7bb4389SLuca Ceresoli # Extract directory name into component name: 164*e7bb4389SLuca Ceresoli # "./cs42l51.0-004a/dapm" -> "cs42l51.0-004a" 165*e7bb4389SLuca Ceresoli c_name="$(basename $(dirname "${c_dir}"))" 166*e7bb4389SLuca Ceresoli fi 167*e7bb4389SLuca Ceresoli 168*e7bb4389SLuca Ceresoli dbg_echo " * Component: ${c_name}" 169*e7bb4389SLuca Ceresoli 170*e7bb4389SLuca Ceresoli echo "" >> "${dot_file}" 171*e7bb4389SLuca Ceresoli echo " subgraph \"${c_name}\" {" >> "${dot_file}" 172*e7bb4389SLuca Ceresoli echo " cluster = true" >> "${dot_file}" 173*e7bb4389SLuca Ceresoli echo " label = \"${c_name}\"" >> "${dot_file}" 174*e7bb4389SLuca Ceresoli echo " color=dodgerblue" >> "${dot_file}" 175*e7bb4389SLuca Ceresoli 176*e7bb4389SLuca Ceresoli # Create empty file to ensure it will exist in all cases 177*e7bb4389SLuca Ceresoli >"${links_file}" 178*e7bb4389SLuca Ceresoli 179*e7bb4389SLuca Ceresoli # Iterate over widgets in the component dir 180*e7bb4389SLuca Ceresoli for w_file in ${c_dir}/*; do 181*e7bb4389SLuca Ceresoli process_dapm_widget "${tmp_dir}" "${c_name}" "${w_file}" 182*e7bb4389SLuca Ceresoli done 183*e7bb4389SLuca Ceresoli 184*e7bb4389SLuca Ceresoli echo " }" >> "${dot_file}" 185*e7bb4389SLuca Ceresoli 186*e7bb4389SLuca Ceresoli cat "${links_file}" >> "${dot_file}" 187*e7bb4389SLuca Ceresoli} 188*e7bb4389SLuca Ceresoli 189*e7bb4389SLuca Ceresoli# Parse the DAPM tree for a sound card and generate graph description in 190*e7bb4389SLuca Ceresoli# graphviz dot format 191*e7bb4389SLuca Ceresoli# 192*e7bb4389SLuca Ceresoli# $1 = temporary work dir 193*e7bb4389SLuca Ceresoli# $2 = directory tree with DAPM state (either in debugfs or a mirror) 194*e7bb4389SLuca Ceresoliprocess_dapm_tree() 195*e7bb4389SLuca Ceresoli{ 196*e7bb4389SLuca Ceresoli local tmp_dir="${1}" 197*e7bb4389SLuca Ceresoli local dapm_dir="${2}" 198*e7bb4389SLuca Ceresoli local dot_file="${tmp_dir}/main.dot" 199*e7bb4389SLuca Ceresoli 200*e7bb4389SLuca Ceresoli echo "digraph G {" > "${dot_file}" 201*e7bb4389SLuca Ceresoli echo " fontname=\"sans-serif\"" >> "${dot_file}" 202*e7bb4389SLuca Ceresoli echo " node [fontname=\"sans-serif\"]" >> "${dot_file}" 203*e7bb4389SLuca Ceresoli 204*e7bb4389SLuca Ceresoli 205*e7bb4389SLuca Ceresoli # Process root directory (no component) 206*e7bb4389SLuca Ceresoli process_dapm_component "${tmp_dir}" "${dapm_dir}/dapm" "ROOT" 207*e7bb4389SLuca Ceresoli 208*e7bb4389SLuca Ceresoli # Iterate over components 209*e7bb4389SLuca Ceresoli for c_dir in "${dapm_dir}"/*/dapm 210*e7bb4389SLuca Ceresoli do 211*e7bb4389SLuca Ceresoli process_dapm_component "${tmp_dir}" "${c_dir}" "" 212*e7bb4389SLuca Ceresoli done 213*e7bb4389SLuca Ceresoli 214*e7bb4389SLuca Ceresoli echo "}" >> "${dot_file}" 215*e7bb4389SLuca Ceresoli} 216*e7bb4389SLuca Ceresoli 217*e7bb4389SLuca Ceresolimain() 218*e7bb4389SLuca Ceresoli{ 219*e7bb4389SLuca Ceresoli # Parse command line 220*e7bb4389SLuca Ceresoli local out_file="dapm.dot" 221*e7bb4389SLuca Ceresoli local card_name="" 222*e7bb4389SLuca Ceresoli local remote_target="" 223*e7bb4389SLuca Ceresoli local dapm_tree="" 224*e7bb4389SLuca Ceresoli local dbg_on="" 225*e7bb4389SLuca Ceresoli while getopts "c:r:d:o:Dh" arg; do 226*e7bb4389SLuca Ceresoli case $arg in 227*e7bb4389SLuca Ceresoli c) card_name="${OPTARG}" ;; 228*e7bb4389SLuca Ceresoli r) remote_target="${OPTARG}" ;; 229*e7bb4389SLuca Ceresoli d) dapm_tree="${OPTARG}" ;; 230*e7bb4389SLuca Ceresoli o) out_file="${OPTARG}" ;; 231*e7bb4389SLuca Ceresoli D) dbg_on="1" ;; 232*e7bb4389SLuca Ceresoli h) usage 0 ;; 233*e7bb4389SLuca Ceresoli *) usage 1 ;; 234*e7bb4389SLuca Ceresoli esac 235*e7bb4389SLuca Ceresoli done 236*e7bb4389SLuca Ceresoli shift $(($OPTIND - 1)) 237*e7bb4389SLuca Ceresoli 238*e7bb4389SLuca Ceresoli if [ -n "${dapm_tree}" ]; then 239*e7bb4389SLuca Ceresoli if [ -n "${card_name}${remote_target}" ]; then 240*e7bb4389SLuca Ceresoli usage 1 "Cannot use -c and -r with -d" 241*e7bb4389SLuca Ceresoli fi 242*e7bb4389SLuca Ceresoli echo "Using local tree: ${dapm_tree}" 243*e7bb4389SLuca Ceresoli elif [ -n "${remote_target}" ]; then 244*e7bb4389SLuca Ceresoli if [ -z "${card_name}" ]; then 245*e7bb4389SLuca Ceresoli usage 1 "-r requires -c" 246*e7bb4389SLuca Ceresoli fi 247*e7bb4389SLuca Ceresoli echo "Using card ${card_name} from remote target ${remote_target}" 248*e7bb4389SLuca Ceresoli elif [ -n "${card_name}" ]; then 249*e7bb4389SLuca Ceresoli echo "Using local card: ${card_name}" 250*e7bb4389SLuca Ceresoli else 251*e7bb4389SLuca Ceresoli usage 1 "Please choose mode using -c, -r or -d" 252*e7bb4389SLuca Ceresoli fi 253*e7bb4389SLuca Ceresoli 254*e7bb4389SLuca Ceresoli # Define logging function 255*e7bb4389SLuca Ceresoli if [ "${dbg_on}" ]; then 256*e7bb4389SLuca Ceresoli dbg_echo() { 257*e7bb4389SLuca Ceresoli echo "$*" >&2 258*e7bb4389SLuca Ceresoli } 259*e7bb4389SLuca Ceresoli else 260*e7bb4389SLuca Ceresoli dbg_echo() { 261*e7bb4389SLuca Ceresoli : 262*e7bb4389SLuca Ceresoli } 263*e7bb4389SLuca Ceresoli fi 264*e7bb4389SLuca Ceresoli 265*e7bb4389SLuca Ceresoli # Filename must have a dot in order the infer the format from the 266*e7bb4389SLuca Ceresoli # extension 267*e7bb4389SLuca Ceresoli if ! echo "${out_file}" | grep -qE '\.'; then 268*e7bb4389SLuca Ceresoli echo "Missing extension in output filename ${out_file}" >&2 269*e7bb4389SLuca Ceresoli usage 270*e7bb4389SLuca Ceresoli exit 1 271*e7bb4389SLuca Ceresoli fi 272*e7bb4389SLuca Ceresoli 273*e7bb4389SLuca Ceresoli local out_fmt="${out_file##*.}" 274*e7bb4389SLuca Ceresoli local dot_file="${out_file%.*}.dot" 275*e7bb4389SLuca Ceresoli 276*e7bb4389SLuca Ceresoli dbg_echo "dot file: $dot_file" 277*e7bb4389SLuca Ceresoli dbg_echo "Output file: $out_file" 278*e7bb4389SLuca Ceresoli dbg_echo "Output format: $out_fmt" 279*e7bb4389SLuca Ceresoli 280*e7bb4389SLuca Ceresoli tmp_dir="$(mktemp -d /tmp/$(basename $0).XXXXXX)" 281*e7bb4389SLuca Ceresoli trap "{ rm -fr ${tmp_dir}; }" INT TERM EXIT 282*e7bb4389SLuca Ceresoli 283*e7bb4389SLuca Ceresoli if [ -z "${dapm_tree}" ] 284*e7bb4389SLuca Ceresoli then 285*e7bb4389SLuca Ceresoli dapm_tree="/sys/kernel/debug/asoc/${card_name}" 286*e7bb4389SLuca Ceresoli fi 287*e7bb4389SLuca Ceresoli if [ -n "${remote_target}" ]; then 288*e7bb4389SLuca Ceresoli dapm_tree="${tmp_dir}/dapm-tree" 289*e7bb4389SLuca Ceresoli grab_remote_files "${remote_target}" "${card_name}" "${tmp_dir}" "${dapm_tree}" 290*e7bb4389SLuca Ceresoli fi 291*e7bb4389SLuca Ceresoli # In all cases now ${dapm_tree} contains the DAPM state 292*e7bb4389SLuca Ceresoli 293*e7bb4389SLuca Ceresoli process_dapm_tree "${tmp_dir}" "${dapm_tree}" 294*e7bb4389SLuca Ceresoli cp "${tmp_dir}/main.dot" "${dot_file}" 295*e7bb4389SLuca Ceresoli 296*e7bb4389SLuca Ceresoli if [ "${out_file}" != "${dot_file}" ]; then 297*e7bb4389SLuca Ceresoli dot -T"${out_fmt}" "${dot_file}" -o "${out_file}" 298*e7bb4389SLuca Ceresoli fi 299*e7bb4389SLuca Ceresoli 300*e7bb4389SLuca Ceresoli echo "Generated file ${out_file}" 301*e7bb4389SLuca Ceresoli} 302*e7bb4389SLuca Ceresoli 303*e7bb4389SLuca Ceresolimain "${@}" 304