Created
October 21, 2025 20:49
-
-
Save andrewleech/7057291b46764f0f5852b0126db2586f to your computer and use it in GitHub Desktop.
MicroPython device version checker - queries connected devices and reports firmware versions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 "$@" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example output: