summaryrefslogtreecommitdiff
path: root/tools/sound/dapm-graph
blob: f14bdfedee8f11507a6b7b04f6dd1847513e6da8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
#!/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 <luca.ceresol@bootlin.com>

set -eu

STYLE_COMPONENT_ON="color=dodgerblue;style=bold"
STYLE_COMPONENT_OFF="color=gray40;style=filled;fillcolor=gray90"
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_route=$(echo "$line" | awk -F\" '{print $2}')
		local w_src=$(echo "$line" |
				  awk -F\" '{print $6 "_" $4}' |
				  sed  's/^(null)_/ROOT_/')
		dbg_echo "     - Input route from: ${w_src}"
		dbg_echo "     - Route: ${w_route}"
		local w_edge_attrs=""
		if [ "${w_route}" != "static" ]; then
		    w_edge_attrs=" [label=\"${w_route}\"]"
		fi
		echo "  \"${w_src}\" -> \"$w_tag\"${w_edge_attrs}" >> "${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 = "ROOT" for the root card directory, empty otherwise
process_dapm_component()
{
    local tmp_dir="${1}"
    local c_dir="${2}"
    local c_name="${3}"
    local is_component=0
    local dot_file="${tmp_dir}/main.dot"
    local links_file="${tmp_dir}/links.dot"
    local c_attribs=""

    if [ -z "${c_name}" ]; then
	is_component=1

	# 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}"

    if [ ${is_component} = 1 ]; then
	if [ -f "${c_dir}/bias_level" ]; then
	    c_onoff=$(sed -n -e 1p "${c_dir}/bias_level" | awk '{print $1}')
	    dbg_echo "   - bias_level: ${c_onoff}"
	    if [ "$c_onoff" = "On" ]; then
		c_attribs="${STYLE_COMPONENT_ON}"
	    elif [ "$c_onoff" = "Off" ]; then
		c_attribs="${STYLE_COMPONENT_OFF}"
	    fi
	fi

	echo ""                           >> "${dot_file}"
	echo "  subgraph \"${c_name}\" {" >> "${dot_file}"
	echo "    cluster = true"         >> "${dot_file}"
	echo "    label = \"${c_name}\""  >> "${dot_file}"
	echo "    ${c_attribs}"           >> "${dot_file}"
    fi

    # 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

    if [ ${is_component} = 1 ]; then
	echo "  }" >> "${dot_file}"
    fi

    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}"
    echo "  edge [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 "${@}"