xref: /linux/tools/sound/dapm-graph (revision e7bb43898bcf54da7ffb4819a04c8428f7db24db)
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