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