#!/bin/sh # SPDX-License-Identifier: GPL-2.0 # # Generate a graph of the current DAPM state for an audio card # # Copyright 2024 Bootlin # Author: Luca Ceresoli set -eu STYLE_NODE_ON="shape=box,style=bold,color=green4" STYLE_NODE_OFF="shape=box,style=filled,color=gray30,fillcolor=gray95" # Print usage and exit # # $1 = exit return value # $2 = error string (required if $1 != 0) usage() { if [ "${1}" -ne 0 ]; then echo "${2}" >&2 fi echo " Generate a graph of the current DAPM state for an audio card. The DAPM state can be obtained via debugfs for a card on the local host or a remote target, or from a local copy of the debugfs tree for the card. Usage: $(basename $0) [options] -c CARD - Local sound card $(basename $0) [options] -c CARD -r REMOTE_TARGET - Card on remote system $(basename $0) [options] -d STATE_DIR - Local directory Options: -c CARD Sound card to get DAPM state of -r REMOTE_TARGET Get DAPM state from REMOTE_TARGET via SSH and SCP instead of using a local sound card -d STATE_DIR Get DAPM state from a local copy of a debugfs tree -o OUT_FILE Output file (default: dapm.dot) -D Show verbose debugging info -h Print this help and exit The output format is implied by the extension of OUT_FILE: * Use the .dot extension to generate a text graph representation in graphviz dot syntax. * Any other extension is assumed to be a format supported by graphviz for rendering, e.g. 'png', 'svg', and will produce both the .dot file and a picture from it. This requires the 'dot' program from the graphviz package. " exit ${1} } # Connect to a remote target via SSH, collect all DAPM files from debufs # into a tarball and get the tarball via SCP into $3/dapm.tar # # $1 = target as used by ssh and scp, e.g. "root@192.168.1.1" # $2 = sound card name # $3 = temp dir path (present on the host, created on the target) # $4 = local directory to extract the tarball into # # Requires an ssh+scp server, find and tar+gz on the target # # Note: the tarball is needed because plain 'scp -r' from debugfs would # copy only empty files grab_remote_files() { echo "Collecting DAPM state from ${1}" dbg_echo "Collected DAPM state in ${3}" ssh "${1}" " set -eu && cd \"/sys/kernel/debug/asoc/${2}\" && find * -type d -exec mkdir -p ${3}/dapm-tree/{} \; && find * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; && cd ${3}/dapm-tree && tar cf ${3}/dapm.tar ." scp -q "${1}:${3}/dapm.tar" "${3}" mkdir -p "${4}" tar xf "${tmp_dir}/dapm.tar" -C "${4}" } # Parse a widget file and generate graph description in graphviz dot format # # Skips any file named "bias_level". # # $1 = temporary work dir # $2 = component name # $3 = widget filename process_dapm_widget() { local tmp_dir="${1}" local c_name="${2}" local w_file="${3}" local dot_file="${tmp_dir}/main.dot" local links_file="${tmp_dir}/links.dot" local w_name="$(basename "${w_file}")" local w_tag="${c_name}_${w_name}" if [ "${w_name}" = "bias_level" ]; then return 0 fi dbg_echo " + Widget: ${w_name}" cat "${w_file}" | ( read line if echo "${line}" | grep -q ': On ' then local node_style="${STYLE_NODE_ON}" else local node_style="${STYLE_NODE_OFF}" fi local w_type="" while read line; do # Collect widget type if present if echo "${line}" | grep -q '^widget-type '; then local w_type_raw="$(echo "$line" | cut -d ' ' -f 2)" dbg_echo " - Widget type: ${w_type_raw}" # Note: escaping '\n' is tricky to get working with both # bash and busybox ash, so use a '%' here and replace it # later local w_type="%n[${w_type_raw}]" fi # Collect any links. We could use "in" links or "out" links, # let's use "in" links if echo "${line}" | grep -q '^in '; then local w_src=$(echo "$line" | awk -F\" '{print $6 "_" $4}' | sed 's/^(null)_/ROOT_/') dbg_echo " - Input route from: ${w_src}" echo " \"${w_src}\" -> \"$w_tag\"" >> "${links_file}" fi done echo " \"${w_tag}\" [label=\"${w_name}${w_type}\",${node_style}]" | tr '%' '\\' >> "${dot_file}" ) } # Parse the DAPM tree for a sound card component and generate graph # description in graphviz dot format # # $1 = temporary work dir # $2 = component directory # $3 = forced component name (extracted for path if empty) process_dapm_component() { local tmp_dir="${1}" local c_dir="${2}" local c_name="${3}" local dot_file="${tmp_dir}/main.dot" local links_file="${tmp_dir}/links.dot" if [ -z "${c_name}" ]; then # Extract directory name into component name: # "./cs42l51.0-004a/dapm" -> "cs42l51.0-004a" c_name="$(basename $(dirname "${c_dir}"))" fi dbg_echo " * Component: ${c_name}" echo "" >> "${dot_file}" echo " subgraph \"${c_name}\" {" >> "${dot_file}" echo " cluster = true" >> "${dot_file}" echo " label = \"${c_name}\"" >> "${dot_file}" echo " color=dodgerblue" >> "${dot_file}" # Create empty file to ensure it will exist in all cases >"${links_file}" # Iterate over widgets in the component dir for w_file in ${c_dir}/*; do process_dapm_widget "${tmp_dir}" "${c_name}" "${w_file}" done echo " }" >> "${dot_file}" cat "${links_file}" >> "${dot_file}" } # Parse the DAPM tree for a sound card and generate graph description in # graphviz dot format # # $1 = temporary work dir # $2 = directory tree with DAPM state (either in debugfs or a mirror) process_dapm_tree() { local tmp_dir="${1}" local dapm_dir="${2}" local dot_file="${tmp_dir}/main.dot" echo "digraph G {" > "${dot_file}" echo " fontname=\"sans-serif\"" >> "${dot_file}" echo " node [fontname=\"sans-serif\"]" >> "${dot_file}" # Process root directory (no component) process_dapm_component "${tmp_dir}" "${dapm_dir}/dapm" "ROOT" # Iterate over components for c_dir in "${dapm_dir}"/*/dapm do process_dapm_component "${tmp_dir}" "${c_dir}" "" done echo "}" >> "${dot_file}" } main() { # Parse command line local out_file="dapm.dot" local card_name="" local remote_target="" local dapm_tree="" local dbg_on="" while getopts "c:r:d:o:Dh" arg; do case $arg in c) card_name="${OPTARG}" ;; r) remote_target="${OPTARG}" ;; d) dapm_tree="${OPTARG}" ;; o) out_file="${OPTARG}" ;; D) dbg_on="1" ;; h) usage 0 ;; *) usage 1 ;; esac done shift $(($OPTIND - 1)) if [ -n "${dapm_tree}" ]; then if [ -n "${card_name}${remote_target}" ]; then usage 1 "Cannot use -c and -r with -d" fi echo "Using local tree: ${dapm_tree}" elif [ -n "${remote_target}" ]; then if [ -z "${card_name}" ]; then usage 1 "-r requires -c" fi echo "Using card ${card_name} from remote target ${remote_target}" elif [ -n "${card_name}" ]; then echo "Using local card: ${card_name}" else usage 1 "Please choose mode using -c, -r or -d" fi # Define logging function if [ "${dbg_on}" ]; then dbg_echo() { echo "$*" >&2 } else dbg_echo() { : } fi # Filename must have a dot in order the infer the format from the # extension if ! echo "${out_file}" | grep -qE '\.'; then echo "Missing extension in output filename ${out_file}" >&2 usage exit 1 fi local out_fmt="${out_file##*.}" local dot_file="${out_file%.*}.dot" dbg_echo "dot file: $dot_file" dbg_echo "Output file: $out_file" dbg_echo "Output format: $out_fmt" tmp_dir="$(mktemp -d /tmp/$(basename $0).XXXXXX)" trap "{ rm -fr ${tmp_dir}; }" INT TERM EXIT if [ -z "${dapm_tree}" ] then dapm_tree="/sys/kernel/debug/asoc/${card_name}" fi if [ -n "${remote_target}" ]; then dapm_tree="${tmp_dir}/dapm-tree" grab_remote_files "${remote_target}" "${card_name}" "${tmp_dir}" "${dapm_tree}" fi # In all cases now ${dapm_tree} contains the DAPM state process_dapm_tree "${tmp_dir}" "${dapm_tree}" cp "${tmp_dir}/main.dot" "${dot_file}" if [ "${out_file}" != "${dot_file}" ]; then dot -T"${out_fmt}" "${dot_file}" -o "${out_file}" fi echo "Generated file ${out_file}" } main "${@}"