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