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