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