|
| 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 | + |
| 9 | +set -eu |
| 10 | + |
| 11 | +STYLE_NODE_ON="shape=box,style=bold,color=green4" |
| 12 | +STYLE_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) |
| 18 | +usage() |
| 19 | +{ |
| 20 | + if [ "${1}" -ne 0 ]; then |
| 21 | + echo "${2}" >&2 |
| 22 | + fi |
| 23 | + |
| 24 | + echo " |
| 25 | +Generate a graph of the current DAPM state for an audio card. |
| 26 | +
|
| 27 | +The DAPM state can be obtained via debugfs for a card on the local host or |
| 28 | +a remote target, or from a local copy of the debugfs tree for the card. |
| 29 | +
|
| 30 | +Usage: |
| 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 | +
|
| 35 | +Options: |
| 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 | +
|
| 44 | +The 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 |
| 69 | +grab_remote_files() |
| 70 | +{ |
| 71 | + echo "Collecting DAPM state from ${1}" |
| 72 | + dbg_echo "Collected DAPM state in ${3}" |
| 73 | + |
| 74 | + ssh "${1}" " |
| 75 | +set -eu && |
| 76 | +cd \"/sys/kernel/debug/asoc/${2}\" && |
| 77 | +find * -type d -exec mkdir -p ${3}/dapm-tree/{} \; && |
| 78 | +find * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; && |
| 79 | +cd ${3}/dapm-tree && |
| 80 | +tar 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 |
| 94 | +process_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) |
| 154 | +process_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) |
| 194 | +process_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 | + |
| 217 | +main() |
| 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 | + |
| 303 | +main "${@}" |
0 commit comments