Skip to content

Instantly share code, notes, and snippets.

@andrewleech
Created October 21, 2025 20:49
Show Gist options
  • Select an option

  • Save andrewleech/7057291b46764f0f5852b0126db2586f to your computer and use it in GitHub Desktop.

Select an option

Save andrewleech/7057291b46764f0f5852b0126db2586f to your computer and use it in GitHub Desktop.
MicroPython device version checker - queries connected devices and reports firmware versions
#!/bin/bash
# check-devices.sh - Query MicroPython device version information
#
# Usage:
# check-devices.sh [OPTIONS] [DEVICE]
#
# Options:
# -h, --help Show this help message
# -v, --verbose Enable verbose output
# -t, --timeout N Set query timeout in seconds (default: 5)
#
# Examples:
# check-devices.sh # Check all connected devices
# check-devices.sh /dev/ttyACM0 # Check specific device
# check-devices.sh a0 # Check using mpremote shortcut
# check-devices.sh -t 10 /dev/ttyACM0 # Use 10 second timeout
set -euo pipefail
# Disable glob expansion for empty matches
shopt -s nullglob
# Default configuration
TIMEOUT=5
VERBOSE=false
# Colors for output (will be disabled if not TTY)
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
NC=''
fi
# Function to print verbose messages
verbose() {
if [[ "$VERBOSE" == "true" ]]; then
echo -e "${BLUE}[DEBUG]${NC} $*" >&2
fi
}
# Function to show help
show_help() {
cat <<EOF
check-devices.sh - Query MicroPython device version information
Usage:
check-devices.sh [OPTIONS] [DEVICE]
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-t, --timeout N Set query timeout in seconds (default: 5)
Arguments:
DEVICE Device path or mpremote shortcut (optional)
If omitted, checks all connected devices
Examples:
check-devices.sh # Check all devices
check-devices.sh /dev/ttyACM0 # Check specific device
check-devices.sh /dev/serial/by-id/usb-... # Check by stable ID
check-devices.sh a0 # Check using shortcut
check-devices.sh -t 10 -v /dev/ttyACM0 # Verbose with 10s timeout
Device Shortcuts:
a0-a9 -> /dev/ttyACM0-9 (Linux)
u0-u9 -> /dev/ttyUSB0-9 (Linux)
c0-c99 -> COM0-99 (Windows)
EOF
}
# Check for required commands
check_dependencies() {
local missing=()
for cmd in mpremote awk; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
# Check for timeout command (GNU coreutils or separate package)
if ! command -v timeout &>/dev/null; then
missing+=("timeout")
fi
if [[ ${#missing[@]} -gt 0 ]]; then
echo -e "${RED}Error: Missing required commands: ${missing[*]}${NC}" >&2
echo "Please install the missing dependencies and try again." >&2
exit 1
fi
}
# Function to resolve mpremote shortcuts to full device paths
resolve_shortcut() {
local device=$1
# Check if it's a shortcut pattern
if [[ "$device" =~ ^a([0-9]+)$ ]]; then
echo "/dev/ttyACM${BASH_REMATCH[1]}"
elif [[ "$device" =~ ^u([0-9]+)$ ]]; then
echo "/dev/ttyUSB${BASH_REMATCH[1]}"
elif [[ "$device" =~ ^c([0-9]+)$ ]]; then
echo "COM${BASH_REMATCH[1]}"
else
# Not a shortcut, return as-is
echo "$device"
fi
}
# Function to resolve /dev/ttyACM* to /dev/serial/by-id path
resolve_to_by_id() {
local device=$1
# If already a by-id path, return it
if [[ "$device" == /dev/serial/by-id/* ]]; then
echo "$device"
return 0
fi
# Find matching by-id path
if [[ -d /dev/serial/by-id ]]; then
for id_path in /dev/serial/by-id/*; do
# nullglob handles empty directory case
if [[ -L "$id_path" ]]; then
local target
target=$(readlink -f "$id_path" 2>/dev/null || echo "")
if [[ "$target" == "$device" ]]; then
echo "$id_path"
return 0
fi
fi
done
fi
# No by-id path found (not an error)
return 0
}
# Portable parsing of os.uname() output
parse_uname_field() {
local output=$1
local field=$2
# Use sed for portable parsing instead of grep -P
# Match: field='value' and extract value
echo "$output" | sed -n "s/.*${field}='\([^']*\)'.*/\1/p" | head -1
}
# Function to query a single device
query_device() {
local device=$1
local device_id=$2 # Optional device ID/serial
local vid_pid=$3 # Optional vid:pid
echo -e "${BLUE}Querying: ${device}${NC}"
verbose "Device ID: ${device_id:-none}, VID:PID: ${vid_pid:-none}"
# Print known info even before query attempt
local by_id_path
by_id_path=$(resolve_to_by_id "$device" 2>/dev/null || echo "")
echo " TTY Path: ${device}"
if [[ -n "$by_id_path" ]]; then
echo " By-ID Path: ${by_id_path}"
else
echo -e " By-ID Path: ${YELLOW}(not found)${NC}"
fi
if [[ -n "$vid_pid" ]]; then
echo " VID:PID: ${vid_pid}"
fi
if [[ -n "$device_id" ]]; then
echo " Device ID: ${device_id}"
fi
# Query device with timeout
local output
local query_cmd="import os; print(os.uname())"
verbose "Executing: timeout $TIMEOUT mpremote connect \"$device\" exec \"$query_cmd\""
if ! output=$(timeout "$TIMEOUT" mpremote connect "$device" exec "$query_cmd" 2>&1); then
echo -e "${RED}✗ Failed to query MicroPython version${NC}"
# Show error details only in verbose mode
if [[ "$VERBOSE" == "true" ]]; then
echo " Error output:"
while IFS= read -r line; do
echo " $line"
done <<< "$output"
fi
echo ""
return 1
fi
verbose "Received output: $output"
# Parse uname output using portable method
# Expected format: (sysname='pyboard', nodename='pyboard', release='1.22.0', version='v1.22.0 on 2024-01-01', machine='PYBv1.1 with STM32F405RG')
local sysname
local release
local version
local machine
sysname=$(parse_uname_field "$output" "sysname")
release=$(parse_uname_field "$output" "release")
version=$(parse_uname_field "$output" "version")
machine=$(parse_uname_field "$output" "machine")
# Default to "unknown" if parsing failed
sysname=${sysname:-unknown}
release=${release:-unknown}
version=${version:-unknown}
machine=${machine:-unknown}
# Check if parsing failed (all fields unknown indicates bad response)
if [[ "$sysname" == "unknown" ]] || [[ "$release" == "unknown" ]] || [[ "$version" == "unknown" ]] || [[ "$machine" == "unknown" ]]; then
echo -e "${YELLOW}⚠ Incomplete MicroPython version data${NC}"
echo " Machine: ${machine}"
echo " System: ${sysname}"
echo " Release: ${release}"
echo " Version: ${version}"
if [[ "$VERBOSE" == "true" ]]; then
echo " (One or more fields could not be parsed)"
fi
echo ""
return 1
fi
# Print results
echo " Machine: ${machine}"
echo " System: ${sysname}"
echo " Release: ${release}"
echo " Version: ${version}"
echo ""
return 0
}
# Main logic
main() {
# Parse command line options
local device_arg=""
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-t|--timeout)
if [[ -z "${2:-}" ]] || [[ "$2" =~ ^- ]]; then
echo -e "${RED}Error: --timeout requires a numeric argument${NC}" >&2
exit 1
fi
TIMEOUT=$2
shift 2
;;
-*)
echo -e "${RED}Error: Unknown option: $1${NC}" >&2
echo "Use --help for usage information" >&2
exit 1
;;
*)
if [[ -n "$device_arg" ]]; then
echo -e "${RED}Error: Multiple device arguments specified${NC}" >&2
exit 1
fi
device_arg=$1
shift
;;
esac
done
# Check dependencies
verbose "Checking dependencies..."
check_dependencies
# Always get device list to extract IDs
verbose "Getting device list from mpremote..."
local device_list
if ! device_list=$(mpremote connect list 2>&1); then
echo -e "${RED}Error: Failed to list devices${NC}" >&2
echo "$device_list" >&2
exit 1
fi
verbose "Device list output:"
if [[ "$VERBOSE" == "true" ]]; then
echo "$device_list" | sed 's/^/ /' >&2
fi
# Parse device list to extract IDs for all devices
declare -A device_ids # device path -> unique ID
declare -A device_vids # device path -> vid:pid
declare -A all_devices # Track all devices seen
while IFS= read -r line; do
if [[ -z "$line" ]]; then
continue
fi
verbose "Parsing line: $line"
# Extract fields
local dev field2 field3
dev=$(echo "$line" | awk '{print $1}')
field2=$(echo "$line" | awk '{print $2}')
field3=$(echo "$line" | awk '{print $3}')
# Skip /dev/ttyS* devices (non-USB serial ports) always
if [[ "$dev" == /dev/ttyS* ]]; then
verbose "Skipping ttyS device: $dev"
continue
fi
# Determine which field is vid:pid vs serial ID
# VID:PID format: exactly 4 hex digits, colon, 4 hex digits (e.g., 2e8a:000c)
local vid_pid=""
local dev_id=""
if [[ "$field2" =~ ^[0-9a-fA-F]{4}:[0-9a-fA-F]{4}$ ]]; then
# Field 2 is vid:pid, field 3 is serial
vid_pid="$field2"
dev_id="$field3"
verbose " VID:PID in field2: $vid_pid, ID in field3: $dev_id"
elif [[ "$field3" =~ ^[0-9a-fA-F]{4}:[0-9a-fA-F]{4}$ ]]; then
# Field 3 is vid:pid, field 2 is serial
vid_pid="$field3"
dev_id="$field2"
verbose " VID:PID in field3: $vid_pid, ID in field2: $dev_id"
else
# Neither matches VID:PID format exactly
# Try to guess: if field contains colon, assume it's vid:pid
if [[ "$field2" =~ : ]]; then
vid_pid="$field2"
dev_id="$field3"
verbose " Guessing field2 is VID:PID: $vid_pid"
else
dev_id="$field2"
vid_pid="$field3"
verbose " Guessing field2 is ID: $dev_id"
fi
fi
# If format is "id:XXXXX", extract just the XXXXX part
if [[ "$dev_id" =~ ^id:(.+)$ ]]; then
dev_id="${BASH_REMATCH[1]}"
verbose " Extracted ID from 'id:' prefix: $dev_id"
fi
# Store all devices (not just those with VID:PID)
if [[ -n "$dev" ]]; then
all_devices["$dev"]=1
if [[ -n "$vid_pid" ]]; then
device_vids["$dev"]="$vid_pid"
fi
if [[ -n "$dev_id" ]]; then
device_ids["$dev"]="$dev_id"
fi
verbose " Stored device: $dev"
fi
done <<< "$device_list"
if [[ -n "$device_arg" ]]; then
# Single device specified
verbose "Single device mode: $device_arg"
# Resolve shortcut to full path
local resolved_device
resolved_device=$(resolve_shortcut "$device_arg")
verbose "Resolved device path: $resolved_device"
# Check if device exists (unless it's a shortcut that mpremote will handle)
if [[ ! -e "$resolved_device" ]] && [[ "$device_arg" != "$resolved_device" ]]; then
# It was a shortcut, use the original for mpremote
verbose "Using original shortcut for mpremote: $device_arg"
resolved_device="$device_arg"
elif [[ ! -e "$resolved_device" ]] && [[ "$device_arg" == "$resolved_device" ]]; then
# Not a shortcut and doesn't exist
echo -e "${RED}Error: Device not found: ${resolved_device}${NC}" >&2
exit 1
fi
# Skip /dev/ttyS* devices
if [[ "$resolved_device" == /dev/ttyS* ]]; then
echo -e "${YELLOW}Skipping /dev/ttyS* device (non-USB serial port)${NC}" >&2
exit 0
fi
# Get IDs for this device (lookup by resolved path)
local dev_id="${device_ids[$resolved_device]:-}"
local vid_pid="${device_vids[$resolved_device]:-}"
verbose "Device ID: ${dev_id:-none}, VID:PID: ${vid_pid:-none}"
query_device "$device_arg" "$dev_id" "$vid_pid"
else
# Query all devices
echo -e "${BLUE}Discovering MicroPython devices...${NC}"
echo ""
# Build list of devices from all_devices
local devices=()
for dev in "${!all_devices[@]}"; do
# Verify device still exists (handles disconnect race condition)
if [[ -e "$dev" ]]; then
devices+=("$dev")
verbose "Added device to query list: $dev"
else
verbose "Device no longer exists, skipping: $dev"
fi
done
# Sort devices alphabetically for consistent order
if [[ ${#devices[@]} -gt 0 ]]; then
mapfile -t devices < <(printf '%s\n' "${devices[@]}" | sort)
verbose "Sorted device list: ${devices[*]}"
fi
if [[ ${#devices[@]} -eq 0 ]]; then
echo -e "${YELLOW}No MicroPython devices found${NC}"
exit 0
fi
echo -e "${BLUE}Found ${#devices[@]} device(s)${NC}"
echo ""
# Query devices sequentially (first pass)
local failed_devices=()
for dev in "${devices[@]}"; do
local dev_id="${device_ids[$dev]:-}"
local vid_pid="${device_vids[$dev]:-}"
if ! query_device "$dev" "$dev_id" "$vid_pid"; then
failed_devices+=("$dev")
fi
done
# Retry failed devices
if [[ ${#failed_devices[@]} -gt 0 ]]; then
echo -e "${YELLOW}=== Retrying ${#failed_devices[@]} failed device(s) ===${NC}"
echo ""
local still_failed=0
for dev in "${failed_devices[@]}"; do
local dev_id="${device_ids[$dev]:-}"
local vid_pid="${device_vids[$dev]:-}"
if ! query_device "$dev" "$dev_id" "$vid_pid"; then
still_failed=$((still_failed + 1))
fi
done
if [[ $still_failed -gt 0 ]]; then
echo -e "${RED}${still_failed} device(s) still failed after retry${NC}"
else
echo -e "${GREEN}All devices succeeded on retry${NC}"
fi
echo ""
fi
fi
}
# Run main
main "$@"
@andrewleech
Copy link
Author

Example output:

$ check-devices.sh
Discovering MicroPython devices...

Found 3 device(s)

Querying: /dev/ttyACM0
  TTY Path:    /dev/ttyACM0
  By-ID Path:  /dev/serial/by-id/usb-MicroPython_Pyboard_Virtual_Comm_Port_in_FS_Mode_3254335D3037-if00
  VID:PID:     f055:9802
  Device ID:   3254335D3037
  Machine:     PYBD-SF6W with STM32F767IIK
  System:      pyboard
  Release:     1.27.0-preview
  Version:     v1.27.0-preview.325.g9bc8e1b6e3.dirty on 2025-10-12

Querying: /dev/ttyACM1
  TTY Path:    /dev/ttyACM1
  By-ID Path:  /dev/serial/by-id/usb-STMicroelectronics_STM32_STLink_066AFF505655806687082951-if02
  VID:PID:     0483:374b
  Device ID:   066AFF505655806687082951
✗ Failed to query MicroPython version

Querying: /dev/ttyACM2
  TTY Path:    /dev/ttyACM2
  By-ID Path:  /dev/serial/by-id/usb-OpenMV_OpenMV_Virtual_Comm_Port_in_FS_Mode_366337723038-if01
  VID:PID:     37c5:1204
  Device ID:   366337723038
  Machine:     OPENMV4 with STM32H743
  System:      OpenMV4-H7
  Release:     1.26.0
  Version:     46b10ee22 on 2025-10-19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment