scripts: Blackmagic probe flash and debug scripts

- Also includes Linux udev rules for both.
- Fix applications/system_example/BUILD.gn on Mac
- Python helper script to find serial port device paths

Bug: b/310955626
Change-Id: Ib3297eae7af53d585ac637fd29d2eed2c4646f6b
Reviewed-on: https://pigweed-review.googlesource.com/c/gonk/+/177898
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Carlos Chinchilla <cachinchilla@google.com>
Reviewed-by: Rob Mohr <mohrr@google.com>
diff --git a/applications/system_example/BUILD.gn b/applications/system_example/BUILD.gn
index 851a7e7..5892b83 100644
--- a/applications/system_example/BUILD.gn
+++ b/applications/system_example/BUILD.gn
@@ -30,5 +30,7 @@
     "$dir_pw_unit_test:rpc_service",
   ]
 
-  ldflags = [ "-Wl,--print-memory-usage" ]
+  if (pw_build_EXECUTABLE_TARGET_TYPE == "//targets/stm32f769i_disc0_stm32cube:stm32f769i_disc0_stm32cube.size_optimized") {
+    ldflags = [ "-Wl,--print-memory-usage" ]
+  }
 }
diff --git a/scripts/debug.sh b/scripts/debug.sh
new file mode 100755
index 0000000..8b08206
--- /dev/null
+++ b/scripts/debug.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Launch a debug session with gdb-dashboard and miniterm in separate Tmux
+# splits.
+
+# Linux Blackmagic Probe
+DEVBMP="/dev/ttyBmpGdb"
+FOUND_BMP=$(python -m gonk_tools.find_serial_port -p 'Black Magic Probe' -1) && DEVBMP=$FOUND_BMP
+
+DEVGONK="/dev/ttyGonk"
+FOUND_GONK=$(python -m gonk_tools.find_serial_port -p 'GENERIC_F730R8TX' -1) && DEVGONK=$FOUND_GONK
+
+GONK_BAUDRATE=115200
+
+# GDB binary
+GDB=arm-none-eabi-gdb
+which gdb-multiarch && GDB=gdb-multiarch
+
+# List all active panes in this tab. For example:
+#
+#   0 /dev/pts/4 (active)
+#   1 /dev/pts/7
+#   2 /dev/pts/6
+#   3 /dev/pts/5
+#
+tmux_list_panes () {
+    tmux list-panes -F '#P #{pane_tty} #{?pane_active,(active),}'
+}
+
+# Get the current tmux pane in focus
+tmux_get_active_pane_id () {
+    tmux_list_panes | awk '/(active)/ { print $1}'
+}
+
+# Get the pane ID for a given TTY.
+# Useful for performing an action on a pane not in focus.
+tmux_get_pane_id () {
+    tmux_list_panes | awk "\$0~\"${1}\" { print \$1}"
+}
+
+# Get the current pane TTY  for a given TTY
+tmux_get_active_pane_tty () {
+    tmux_list_panes | awk '/(active)/ { print $2}'
+}
+
+
+split_tmux_panes () {
+    ORIGINAL_PANE=$(tmux_get_active_pane_id)
+
+    # Make a split for gdb-dashboard
+    tmux select-pane -t $ORIGINAL_PANE
+    tmux split-window -h
+    DASHBOARD_TTY=$(tmux_get_active_pane_tty)
+
+    # Make a split for serial output
+    tmux select-pane -t $ORIGINAL_PANE
+    tmux split-window -v "sleep 4; python -m serial.tools.miniterm --raw ${DEVGONK} ${GONK_BAUDRATE}"
+    SERIAL_TTY=$(tmux_get_active_pane_tty)
+
+    # Select the original pane and run gdb
+    tmux select-pane -t $ORIGINAL_PANE
+}
+
+tmux_cleanup () {
+    tmux kill-pane -t $(tmux_get_pane_id $SERIAL_TTY)
+    tmux kill-pane -t $(tmux_get_pane_id $DASHBOARD_TTY)
+}
+
+which tmux && split_tmux_panes
+
+$GDB \
+    -ex "set confirm off" \
+    -ex "dashboard -output ${DASHBOARD_TTY}" \
+    -ex "target extended-remote ${DEVBMP}" \
+    -ex "monitor version" \
+    -ex "monitor tpwr enable" \
+    -ex "monitor swdp_scan" \
+    -ex "attach 1" \
+    -ex "load" \
+    -ex "compare-sections" \
+    -ex "run" \
+    $@
+
+which tmux && tmux_cleanup
diff --git a/scripts/flash-with-blackmagic-probe.sh b/scripts/flash-with-blackmagic-probe.sh
new file mode 100755
index 0000000..df3248b
--- /dev/null
+++ b/scripts/flash-with-blackmagic-probe.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Flash an elf using the Blackmagic probe.
+
+DEVBMP="/dev/ttyBmpGdb"
+FOUND_BMP=$(python -m gonk_tools.find_serial_port -p 'Black Magic Probe' -1) && DEVBMP=$FOUND_BMP
+
+arm-none-eabi-gdb -nx --batch \
+    -ex "set confirm off" \
+    -ex "target extended-remote ${DEVBMP}" \
+    -ex "monitor version" \
+    -ex "monitor tpwr enable" \
+    -ex "monitor swdp_scan" \
+    -ex "attach 1" \
+    -ex "load" \
+    -ex "compare-sections" \
+    -ex "kill" \
+    $@
diff --git a/scripts/udev-rules/99-blackmagic-plugdev.rules b/scripts/udev-rules/99-blackmagic-plugdev.rules
new file mode 100644
index 0000000..4b24035
--- /dev/null
+++ b/scripts/udev-rules/99-blackmagic-plugdev.rules
@@ -0,0 +1,16 @@
+# Black Magic Probe
+# There are two connections, one for GDB and one for UART debugging.
+#
+# Copy this to /usr/lib/udev/rules.d/99-blackmagic.rules
+# and run:
+# sudo udevadm control --reload-rules
+# sudo udevadm trigger
+
+ACTION!="add|change", GOTO="blackmagic_rules_end"
+SUBSYSTEM=="tty", ACTION=="add", ATTRS{interface}=="Black Magic GDB Server", SYMLINK+="ttyBmpGdb"
+SUBSYSTEM=="tty", ACTION=="add", ATTRS{interface}=="Black Magic UART Port", SYMLINK+="ttyBmpTarg"
+SUBSYSTEM=="tty", ACTION=="add", ATTRS{interface}=="Black Magic GDB Server", SYMLINK+="ttyBmpGdb%E{ID_SERIAL_SHORT}"
+SUBSYSTEM=="tty", ACTION=="add", ATTRS{interface}=="Black Magic UART Port", SYMLINK+="ttyBmpTarg%E{ID_SERIAL_SHORT}"
+SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="6017", MODE="0666", GROUP="plugdev", TAG+="uaccess"
+SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="6018", MODE="0666", GROUP="plugdev", TAG+="uaccess"
+LABEL="blackmagic_rules_end"
diff --git a/scripts/udev-rules/99-gonk.rules b/scripts/udev-rules/99-gonk.rules
new file mode 100644
index 0000000..d546903
--- /dev/null
+++ b/scripts/udev-rules/99-gonk.rules
@@ -0,0 +1,7 @@
+# Copy this to /usr/lib/udev/rules.d/99-gonk.rules
+# and run:
+# sudo udevadm control --reload-rules
+# sudo udevadm trigger
+
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE:="0666", GROUP="plugdev"
+KERNEL=="ttyACM*", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE:="0666", GROUP="plugdev", SYMLINK+="ttyGonk"
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index d37ec86..90dc3d4 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -24,6 +24,7 @@
   sources = [
     "gonk_tools/__init__.py",
     "gonk_tools/build_project.py",
+    "gonk_tools/find_serial_port.py",
     "gonk_tools/presubmit_checks.py",
   ]
   python_deps = [
diff --git a/tools/gonk_tools/find_serial_port.py b/tools/gonk_tools/find_serial_port.py
new file mode 100644
index 0000000..abf678e
--- /dev/null
+++ b/tools/gonk_tools/find_serial_port.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Find serial ports."""
+
+import argparse
+import operator
+import sys
+from typing import Optional, Sequence
+
+from serial.tools.list_ports import comports
+from serial.tools.list_ports_common import ListPortInfo
+
+
+def _parse_args():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        '-l',
+        '--list-ports',
+        action='store_true',
+        help='List all port info.',
+    )
+    parser.add_argument(
+        '-p',
+        '--product',
+        help='Print ports matching this product name.',
+    )
+    parser.add_argument(
+        '-s',
+        '--serial-number',
+        help='Print ports matching this serial number.',
+    )
+    parser.add_argument(
+        '-1',
+        '--print-first-match',
+        action='store_true',
+        help='Print the first port found sorted by device path.',
+    )
+    return parser.parse_args()
+
+
+def _print_ports(ports: Sequence[ListPortInfo]):
+    for cp in ports:
+        for line in [
+                f"device        = {cp.device}",
+                f"name          = {cp.name}",
+                f"description   = {cp.description}",
+                f"vid           = {cp.vid}",
+                f"pid           = {cp.pid}",
+                f"serial_number = {cp.serial_number}",
+                f"location      = {cp.location}",
+                f"manufacturer  = {cp.manufacturer}",
+                f"product       = {cp.product}",
+                f"interface     = {cp.interface}",
+        ]:
+            print(line)
+        print()
+
+
+def main(
+    list_ports: bool = False,
+    product: Optional[str] = None,
+    serial_number: Optional[str] = None,
+    print_first_match: bool = False,
+) -> int:
+    """List all device info or print matches."""
+    ports = sorted(comports(), key=operator.attrgetter('device'))
+
+    if list_ports:
+        _print_ports(ports)
+        return 0
+
+    any_match_found = False
+
+    # Print matching devices
+    for port in ports:
+        if (product is not None and port.product is not None
+                and product in port.product):
+            any_match_found = True
+            print(port.device)
+
+        if (serial_number is not None and port.serial_number is not None
+                and serial_number in port.serial_number):
+            any_match_found = True
+            print(port.device)
+
+        if any_match_found and print_first_match:
+            return 0
+
+    if not any_match_found:
+        return 1
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(**vars(_parse_args())))