···11+#!/bin/sh22+# SPDX-License-Identifier: GPL-2.033+#44+# Generate a graph of the current DAPM state for an audio card55+#66+# Copyright 2024 Bootlin77+# Author: Luca Ceresoli <luca.ceresol@bootlin.com>88+99+set -eu1010+1111+STYLE_NODE_ON="shape=box,style=bold,color=green4"1212+STYLE_NODE_OFF="shape=box,style=filled,color=gray30,fillcolor=gray95"1313+1414+# Print usage and exit1515+#1616+# $1 = exit return value1717+# $2 = error string (required if $1 != 0)1818+usage()1919+{2020+ if [ "${1}" -ne 0 ]; then2121+ echo "${2}" >&22222+ fi2323+2424+ echo "2525+Generate a graph of the current DAPM state for an audio card.2626+2727+The DAPM state can be obtained via debugfs for a card on the local host or2828+a remote target, or from a local copy of the debugfs tree for the card.2929+3030+Usage:3131+ $(basename $0) [options] -c CARD - Local sound card3232+ $(basename $0) [options] -c CARD -r REMOTE_TARGET - Card on remote system3333+ $(basename $0) [options] -d STATE_DIR - Local directory3434+3535+Options:3636+ -c CARD Sound card to get DAPM state of3737+ -r REMOTE_TARGET Get DAPM state from REMOTE_TARGET via SSH and SCP3838+ instead of using a local sound card3939+ -d STATE_DIR Get DAPM state from a local copy of a debugfs tree4040+ -o OUT_FILE Output file (default: dapm.dot)4141+ -D Show verbose debugging info4242+ -h Print this help and exit4343+4444+The output format is implied by the extension of OUT_FILE:4545+4646+ * Use the .dot extension to generate a text graph representation in4747+ graphviz dot syntax.4848+ * Any other extension is assumed to be a format supported by graphviz for4949+ rendering, e.g. 'png', 'svg', and will produce both the .dot file and a5050+ picture from it. This requires the 'dot' program from the graphviz5151+ package.5252+"5353+5454+ exit ${1}5555+}5656+5757+# Connect to a remote target via SSH, collect all DAPM files from debufs5858+# into a tarball and get the tarball via SCP into $3/dapm.tar5959+#6060+# $1 = target as used by ssh and scp, e.g. "root@192.168.1.1"6161+# $2 = sound card name6262+# $3 = temp dir path (present on the host, created on the target)6363+# $4 = local directory to extract the tarball into6464+#6565+# Requires an ssh+scp server, find and tar+gz on the target6666+#6767+# Note: the tarball is needed because plain 'scp -r' from debugfs would6868+# copy only empty files6969+grab_remote_files()7070+{7171+ echo "Collecting DAPM state from ${1}"7272+ dbg_echo "Collected DAPM state in ${3}"7373+7474+ ssh "${1}" "7575+set -eu &&7676+cd \"/sys/kernel/debug/asoc/${2}\" &&7777+find * -type d -exec mkdir -p ${3}/dapm-tree/{} \; &&7878+find * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; &&7979+cd ${3}/dapm-tree &&8080+tar cf ${3}/dapm.tar ."8181+ scp -q "${1}:${3}/dapm.tar" "${3}"8282+8383+ mkdir -p "${4}"8484+ tar xf "${tmp_dir}/dapm.tar" -C "${4}"8585+}8686+8787+# Parse a widget file and generate graph description in graphviz dot format8888+#8989+# Skips any file named "bias_level".9090+#9191+# $1 = temporary work dir9292+# $2 = component name9393+# $3 = widget filename9494+process_dapm_widget()9595+{9696+ local tmp_dir="${1}"9797+ local c_name="${2}"9898+ local w_file="${3}"9999+ local dot_file="${tmp_dir}/main.dot"100100+ local links_file="${tmp_dir}/links.dot"101101+102102+ local w_name="$(basename "${w_file}")"103103+ local w_tag="${c_name}_${w_name}"104104+105105+ if [ "${w_name}" = "bias_level" ]; then106106+ return 0107107+ fi108108+109109+ dbg_echo " + Widget: ${w_name}"110110+111111+ cat "${w_file}" | (112112+ read line113113+114114+ if echo "${line}" | grep -q ': On '115115+ then local node_style="${STYLE_NODE_ON}"116116+ else local node_style="${STYLE_NODE_OFF}"117117+ fi118118+119119+ local w_type=""120120+ while read line; do121121+ # Collect widget type if present122122+ if echo "${line}" | grep -q '^widget-type '; then123123+ local w_type_raw="$(echo "$line" | cut -d ' ' -f 2)"124124+ dbg_echo " - Widget type: ${w_type_raw}"125125+126126+ # Note: escaping '\n' is tricky to get working with both127127+ # bash and busybox ash, so use a '%' here and replace it128128+ # later129129+ local w_type="%n[${w_type_raw}]"130130+ fi131131+132132+ # Collect any links. We could use "in" links or "out" links,133133+ # let's use "in" links134134+ if echo "${line}" | grep -q '^in '; then135135+ local w_src=$(echo "$line" |136136+ awk -F\" '{print $6 "_" $4}' |137137+ sed 's/^(null)_/ROOT_/')138138+ dbg_echo " - Input route from: ${w_src}"139139+ echo " \"${w_src}\" -> \"$w_tag\"" >> "${links_file}"140140+ fi141141+ done142142+143143+ echo " \"${w_tag}\" [label=\"${w_name}${w_type}\",${node_style}]" |144144+ tr '%' '\\' >> "${dot_file}"145145+ )146146+}147147+148148+# Parse the DAPM tree for a sound card component and generate graph149149+# description in graphviz dot format150150+#151151+# $1 = temporary work dir152152+# $2 = component directory153153+# $3 = forced component name (extracted for path if empty)154154+process_dapm_component()155155+{156156+ local tmp_dir="${1}"157157+ local c_dir="${2}"158158+ local c_name="${3}"159159+ local dot_file="${tmp_dir}/main.dot"160160+ local links_file="${tmp_dir}/links.dot"161161+162162+ if [ -z "${c_name}" ]; then163163+ # Extract directory name into component name:164164+ # "./cs42l51.0-004a/dapm" -> "cs42l51.0-004a"165165+ c_name="$(basename $(dirname "${c_dir}"))"166166+ fi167167+168168+ dbg_echo " * Component: ${c_name}"169169+170170+ echo "" >> "${dot_file}"171171+ echo " subgraph \"${c_name}\" {" >> "${dot_file}"172172+ echo " cluster = true" >> "${dot_file}"173173+ echo " label = \"${c_name}\"" >> "${dot_file}"174174+ echo " color=dodgerblue" >> "${dot_file}"175175+176176+ # Create empty file to ensure it will exist in all cases177177+ >"${links_file}"178178+179179+ # Iterate over widgets in the component dir180180+ for w_file in ${c_dir}/*; do181181+ process_dapm_widget "${tmp_dir}" "${c_name}" "${w_file}"182182+ done183183+184184+ echo " }" >> "${dot_file}"185185+186186+ cat "${links_file}" >> "${dot_file}"187187+}188188+189189+# Parse the DAPM tree for a sound card and generate graph description in190190+# graphviz dot format191191+#192192+# $1 = temporary work dir193193+# $2 = directory tree with DAPM state (either in debugfs or a mirror)194194+process_dapm_tree()195195+{196196+ local tmp_dir="${1}"197197+ local dapm_dir="${2}"198198+ local dot_file="${tmp_dir}/main.dot"199199+200200+ echo "digraph G {" > "${dot_file}"201201+ echo " fontname=\"sans-serif\"" >> "${dot_file}"202202+ echo " node [fontname=\"sans-serif\"]" >> "${dot_file}"203203+204204+205205+ # Process root directory (no component)206206+ process_dapm_component "${tmp_dir}" "${dapm_dir}/dapm" "ROOT"207207+208208+ # Iterate over components209209+ for c_dir in "${dapm_dir}"/*/dapm210210+ do211211+ process_dapm_component "${tmp_dir}" "${c_dir}" ""212212+ done213213+214214+ echo "}" >> "${dot_file}"215215+}216216+217217+main()218218+{219219+ # Parse command line220220+ local out_file="dapm.dot"221221+ local card_name=""222222+ local remote_target=""223223+ local dapm_tree=""224224+ local dbg_on=""225225+ while getopts "c:r:d:o:Dh" arg; do226226+ case $arg in227227+ c) card_name="${OPTARG}" ;;228228+ r) remote_target="${OPTARG}" ;;229229+ d) dapm_tree="${OPTARG}" ;;230230+ o) out_file="${OPTARG}" ;;231231+ D) dbg_on="1" ;;232232+ h) usage 0 ;;233233+ *) usage 1 ;;234234+ esac235235+ done236236+ shift $(($OPTIND - 1))237237+238238+ if [ -n "${dapm_tree}" ]; then239239+ if [ -n "${card_name}${remote_target}" ]; then240240+ usage 1 "Cannot use -c and -r with -d"241241+ fi242242+ echo "Using local tree: ${dapm_tree}"243243+ elif [ -n "${remote_target}" ]; then244244+ if [ -z "${card_name}" ]; then245245+ usage 1 "-r requires -c"246246+ fi247247+ echo "Using card ${card_name} from remote target ${remote_target}"248248+ elif [ -n "${card_name}" ]; then249249+ echo "Using local card: ${card_name}"250250+ else251251+ usage 1 "Please choose mode using -c, -r or -d"252252+ fi253253+254254+ # Define logging function255255+ if [ "${dbg_on}" ]; then256256+ dbg_echo() {257257+ echo "$*" >&2258258+ }259259+ else260260+ dbg_echo() {261261+ :262262+ }263263+ fi264264+265265+ # Filename must have a dot in order the infer the format from the266266+ # extension267267+ if ! echo "${out_file}" | grep -qE '\.'; then268268+ echo "Missing extension in output filename ${out_file}" >&2269269+ usage270270+ exit 1271271+ fi272272+273273+ local out_fmt="${out_file##*.}"274274+ local dot_file="${out_file%.*}.dot"275275+276276+ dbg_echo "dot file: $dot_file"277277+ dbg_echo "Output file: $out_file"278278+ dbg_echo "Output format: $out_fmt"279279+280280+ tmp_dir="$(mktemp -d /tmp/$(basename $0).XXXXXX)"281281+ trap "{ rm -fr ${tmp_dir}; }" INT TERM EXIT282282+283283+ if [ -z "${dapm_tree}" ]284284+ then285285+ dapm_tree="/sys/kernel/debug/asoc/${card_name}"286286+ fi287287+ if [ -n "${remote_target}" ]; then288288+ dapm_tree="${tmp_dir}/dapm-tree"289289+ grab_remote_files "${remote_target}" "${card_name}" "${tmp_dir}" "${dapm_tree}"290290+ fi291291+ # In all cases now ${dapm_tree} contains the DAPM state292292+293293+ process_dapm_tree "${tmp_dir}" "${dapm_tree}"294294+ cp "${tmp_dir}/main.dot" "${dot_file}"295295+296296+ if [ "${out_file}" != "${dot_file}" ]; then297297+ dot -T"${out_fmt}" "${dot_file}" -o "${out_file}"298298+ fi299299+300300+ echo "Generated file ${out_file}"301301+}302302+303303+main "${@}"