Compare commits
2 commits
main
...
offline-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d65cc7368a | ||
|
|
52d04e94ff |
9 changed files with 2922 additions and 0 deletions
215
download_firmware.ps1
Normal file
215
download_firmware.ps1
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# PowerShell script to download WipperSnapper offline firmware assets
|
||||
# This script fetches the latest offline release assets and saves them to a local folder
|
||||
|
||||
# Configuration
|
||||
$Repo = "adafruit/Adafruit_Wippersnapper_Arduino"
|
||||
$OutputDir = "latest_firmware"
|
||||
$ReleasesApiUrl = "https://api.github.com/repos/$Repo/releases"
|
||||
$PerPage = 30 # GitHub API default per page limit
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
if (-not (Test-Path -Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir | Out-Null
|
||||
Write-Host "Created directory: $OutputDir"
|
||||
}
|
||||
|
||||
Write-Host "Fetching releases information for $Repo..."
|
||||
|
||||
try {
|
||||
# Use TLS 1.2 for GitHub API
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Fetch the releases data with pagination
|
||||
$Headers = @{
|
||||
"Accept" = "application/vnd.github.v3+json"
|
||||
}
|
||||
|
||||
$AllReleases = @()
|
||||
$Page = 1
|
||||
$FoundOfflineRelease = $false
|
||||
$LatestOfflineRelease = $null
|
||||
|
||||
# Loop through pages until we find an offline release or run out of releases
|
||||
do {
|
||||
$PageUrl = "$ReleasesApiUrl`?page=$Page&per_page=$PerPage"
|
||||
Write-Host "Fetching page $Page of releases..."
|
||||
|
||||
$PageReleases = Invoke-RestMethod -Uri $PageUrl -Headers $Headers -ErrorAction Stop
|
||||
|
||||
# Add to all releases - ensure we're adding to an array
|
||||
if ($PageReleases -is [array]) {
|
||||
$AllReleases += $PageReleases
|
||||
} else {
|
||||
# If it's a single object, wrap it in an array before adding
|
||||
$AllReleases += @($PageReleases)
|
||||
}
|
||||
|
||||
# Check if this page contains any offline releases
|
||||
$OfflineReleases = @($PageReleases | Where-Object { $_.tag_name -like "*offline*" })
|
||||
|
||||
# Force result to be an array using @() and check count
|
||||
$OfflineReleasesCount = @($OfflineReleases).Count
|
||||
Write-Host "Found $OfflineReleasesCount offline releases on page $Page"
|
||||
|
||||
if ($OfflineReleasesCount -gt 0) {
|
||||
$FoundOfflineRelease = $true
|
||||
# Get the latest offline release by published date
|
||||
$LatestOfflineRelease = $OfflineReleases |
|
||||
Sort-Object -Property published_at -Descending |
|
||||
Select-Object -First 1
|
||||
break
|
||||
}
|
||||
|
||||
# Move to next page if we didn't find any offline releases and there are more pages
|
||||
$Page++
|
||||
|
||||
# Stop if we received fewer items than the per_page limit (meaning we're on the last page)
|
||||
# Force result to be an array using @() and check count
|
||||
$PageReleasesCount = @($PageReleases).Count
|
||||
if ($PageReleasesCount -lt $PerPage) {
|
||||
Write-Host "Reached the last page with $PageReleasesCount items (less than $PerPage per page)"
|
||||
break
|
||||
}
|
||||
} while (-not $FoundOfflineRelease)
|
||||
|
||||
# Check if we found an offline release
|
||||
if ($null -eq $LatestOfflineRelease) {
|
||||
Write-Host "Error: No offline release found after checking $Page pages of releases."
|
||||
exit
|
||||
}
|
||||
|
||||
$TagName = $LatestOfflineRelease.tag_name
|
||||
$ReleaseName = $LatestOfflineRelease.name
|
||||
$ReleaseId = $LatestOfflineRelease.id
|
||||
|
||||
Write-Host "Found latest offline release: $ReleaseName (Tag: $TagName, ID: $ReleaseId)"
|
||||
|
||||
# Fetch the complete assets list directly from the assets URL
|
||||
$AssetsUrl = "https://api.github.com/repos/$Repo/releases/$ReleaseId/assets"
|
||||
Write-Host "Fetching complete assets list from: $AssetsUrl"
|
||||
|
||||
$AllAssets = @()
|
||||
$AssetsPage = 1
|
||||
|
||||
# Loop through pages to get all assets
|
||||
do {
|
||||
$AssetsPageUrl = "$AssetsUrl`?page=$AssetsPage&per_page=$PerPage"
|
||||
Write-Host "Fetching page $AssetsPage of assets..."
|
||||
|
||||
$PageAssets = Invoke-RestMethod -Uri $AssetsPageUrl -Headers $Headers -ErrorAction Stop
|
||||
|
||||
# Add to all assets - ensure we're adding to an array
|
||||
if ($PageAssets -is [array]) {
|
||||
$AllAssets += $PageAssets
|
||||
} else {
|
||||
# If it's a single object, wrap it in an array before adding
|
||||
$AllAssets += @($PageAssets)
|
||||
}
|
||||
|
||||
# Move to next page if there are more assets
|
||||
$AssetsPage++
|
||||
|
||||
# Stop if we received fewer items than the per_page limit (meaning we're on the last page)
|
||||
# Force result to be an array using @() and check count
|
||||
$PageAssetsCount = @($PageAssets).Count
|
||||
if ($PageAssetsCount -lt $PerPage) {
|
||||
Write-Host "Reached the last page of assets with $PageAssetsCount items"
|
||||
break
|
||||
}
|
||||
} while ($true)
|
||||
|
||||
# Filter for UF2 and BIN files
|
||||
Write-Host "Filtering for firmware files..."
|
||||
$FirmwareAssets = @($AllAssets | Where-Object { $_.name -like "*.uf2" -or $_.name -like "*.bin" })
|
||||
|
||||
# Check if we found any assets
|
||||
$FirmwareAssetsCount = @($FirmwareAssets).Count
|
||||
if ($FirmwareAssetsCount -eq 0) {
|
||||
Write-Host "No firmware files (.uf2 or .bin) found in release $TagName."
|
||||
exit
|
||||
}
|
||||
|
||||
Write-Host "Found $FirmwareAssetsCount firmware files to download."
|
||||
|
||||
# Download each asset
|
||||
foreach ($Asset in $FirmwareAssets) {
|
||||
$Name = $Asset.name
|
||||
$Url = $Asset.browser_download_url
|
||||
$Size = $Asset.size
|
||||
$OutputFile = Join-Path -Path $OutputDir -ChildPath $Name
|
||||
|
||||
# Check if file already exists
|
||||
if (Test-Path -Path $OutputFile) {
|
||||
Write-Host "File $Name already exists, checking if it's the same version..."
|
||||
|
||||
# Get file info
|
||||
$ExistingFile = Get-Item -Path $OutputFile
|
||||
|
||||
# Check if file size matches
|
||||
if ($ExistingFile.Length -eq $Size) {
|
||||
# Calculate SHA256 hash of the existing file
|
||||
$FileHash = Get-FileHash -Path $OutputFile -Algorithm SHA256
|
||||
|
||||
# Get the remote file's hash by downloading the first few bytes
|
||||
try {
|
||||
# First try to get the hash from the GitHub API if available
|
||||
if ($Asset.PSObject.Properties.Name -contains "sha256") {
|
||||
$RemoteHash = $Asset.sha256
|
||||
} else {
|
||||
# If GitHub doesn't provide the hash, we'll compute it from the remote file
|
||||
# This is a temporary file to download the remote file for hash verification
|
||||
$TempFile = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
# Download the file to temp location
|
||||
Invoke-WebRequest -Uri $Url -OutFile $TempFile -ErrorAction Stop
|
||||
|
||||
# Calculate hash
|
||||
$RemoteFileHash = Get-FileHash -Path $TempFile -Algorithm SHA256
|
||||
$RemoteHash = $RemoteFileHash.Hash
|
||||
|
||||
# Remove temp file
|
||||
Remove-Item -Path $TempFile -Force
|
||||
}
|
||||
|
||||
# Compare hashes
|
||||
if ($FileHash.Hash -eq $RemoteHash) {
|
||||
Write-Host "File $Name is already up to date (hash verified), skipping download."
|
||||
continue
|
||||
} else {
|
||||
Write-Host "File $Name has different hash, downloading new version..."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Could not verify hash for $Name, will download again to be safe."
|
||||
}
|
||||
} else {
|
||||
Write-Host "File $Name has different size, downloading new version..."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Downloading $Name..."
|
||||
}
|
||||
|
||||
# Download the file
|
||||
try {
|
||||
Invoke-WebRequest -Uri $Url -OutFile $OutputFile -ErrorAction Stop
|
||||
Write-Host "Successfully downloaded $Name"
|
||||
}
|
||||
catch {
|
||||
Write-Host "Failed to download $Name. Error: $_"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Download complete. Firmware files saved to $OutputDir\"
|
||||
Get-ChildItem -Path $OutputDir | Format-Table Name, Length
|
||||
}
|
||||
catch {
|
||||
if ($_.Exception.Message -like "*404*") {
|
||||
Write-Host "Error: Repository or releases not found."
|
||||
}
|
||||
elseif ($_.Exception.Message -like "*rate limit*") {
|
||||
Write-Host "Error: GitHub API rate limit exceeded. Try again later or use an API token."
|
||||
}
|
||||
else {
|
||||
Write-Host "Error fetching release data: $_"
|
||||
}
|
||||
}
|
||||
209
download_firmware.sh
Normal file
209
download_firmware.sh
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script to download WipperSnapper offline firmware assets
|
||||
# This script fetches the latest offline release assets and saves them to a local folder
|
||||
|
||||
# Configuration
|
||||
REPO="adafruit/Adafruit_Wippersnapper_Arduino"
|
||||
OUTPUT_DIR="latest_firmware"
|
||||
RELEASES_API_URL="https://api.github.com/repos/$REPO/releases"
|
||||
PER_PAGE=30 # GitHub API default per page limit
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "Fetching releases information for $REPO..."
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required but not installed. Please install jq first."
|
||||
echo "On Debian/Ubuntu: sudo apt-get install jq"
|
||||
echo "On macOS with Homebrew: brew install jq"
|
||||
echo "On Windows with Chocolatey: choco install jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if sha256sum is installed
|
||||
if ! command -v sha256sum &> /dev/null; then
|
||||
# Try to use shasum as an alternative on macOS
|
||||
if command -v shasum &> /dev/null; then
|
||||
SHA256_CMD="shasum -a 256"
|
||||
else
|
||||
echo "Warning: Neither sha256sum nor shasum found. Hash verification will be skipped."
|
||||
SHA256_CMD="echo 'Hash verification not available for'"
|
||||
fi
|
||||
else
|
||||
SHA256_CMD="sha256sum"
|
||||
fi
|
||||
|
||||
# Initialize variables
|
||||
page=1
|
||||
found_offline_release=false
|
||||
latest_offline_release=""
|
||||
latest_published_at=""
|
||||
|
||||
# Loop through pages until we find an offline release or run out of releases
|
||||
while [ "$found_offline_release" = false ]; do
|
||||
echo "Fetching page $page of releases..."
|
||||
page_url="${RELEASES_API_URL}?page=${page}&per_page=${PER_PAGE}"
|
||||
|
||||
# Fetch the page of releases
|
||||
page_releases=$(curl -s "$page_url")
|
||||
|
||||
# Check if the API call was successful
|
||||
if echo "$page_releases" | grep -q "API rate limit exceeded"; then
|
||||
echo "Error: GitHub API rate limit exceeded. Try again later or use an API token."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we got an empty array (end of pages)
|
||||
if [ "$(echo "$page_releases" | jq 'length')" = "0" ]; then
|
||||
echo "No more releases found after checking $page pages."
|
||||
break
|
||||
fi
|
||||
|
||||
# Find offline releases in this page - using "*offline*" instead of "*-offline*"
|
||||
offline_releases=$(echo "$page_releases" | jq '[.[] | select(.tag_name | contains("offline"))]')
|
||||
|
||||
# Check if we found any offline releases
|
||||
offline_count=$(echo "$offline_releases" | jq 'length')
|
||||
echo "Found $offline_count offline releases on page $page"
|
||||
|
||||
if [ "$offline_count" -gt 0 ]; then
|
||||
found_offline_release=true
|
||||
|
||||
# Get the latest offline release by published_at date
|
||||
latest_offline_release=$(echo "$offline_releases" | jq 'sort_by(.published_at) | reverse | .[0]')
|
||||
else
|
||||
# Move to the next page
|
||||
page=$((page + 1))
|
||||
|
||||
# Check if we received fewer items than the per_page limit (meaning we're on the last page)
|
||||
page_count=$(echo "$page_releases" | jq 'length')
|
||||
if [ "$page_count" -lt "$PER_PAGE" ]; then
|
||||
echo "Reached the last page with $page_count items (less than $PER_PAGE per page)"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if we found an offline release
|
||||
if [ -z "$latest_offline_release" ] || [ "$latest_offline_release" = "null" ]; then
|
||||
echo "Error: No offline release found after checking $page pages of releases."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract tag name, release name, and release ID
|
||||
tag_name=$(echo "$latest_offline_release" | jq -r '.tag_name')
|
||||
release_name=$(echo "$latest_offline_release" | jq -r '.name')
|
||||
release_id=$(echo "$latest_offline_release" | jq -r '.id')
|
||||
|
||||
echo "Found latest offline release: $release_name (Tag: $tag_name, ID: $release_id)"
|
||||
|
||||
# Fetch the complete assets list directly from the assets URL
|
||||
assets_url="https://api.github.com/repos/$REPO/releases/$release_id/assets"
|
||||
echo "Fetching complete assets list from: $assets_url"
|
||||
|
||||
# Initialize variables for assets pagination
|
||||
assets_page=1
|
||||
all_assets="[]"
|
||||
|
||||
# Loop through pages to get all assets
|
||||
while true; do
|
||||
echo "Fetching page $assets_page of assets..."
|
||||
assets_page_url="${assets_url}?page=${assets_page}&per_page=${PER_PAGE}"
|
||||
|
||||
# Fetch the page of assets
|
||||
page_assets=$(curl -s "$assets_page_url")
|
||||
|
||||
# Check if we got an empty array (end of pages)
|
||||
page_assets_count=$(echo "$page_assets" | jq 'length')
|
||||
if [ "$page_assets_count" = "0" ]; then
|
||||
echo "No more assets found."
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Found $page_assets_count assets on page $assets_page"
|
||||
|
||||
# Add this page's assets to our collection
|
||||
all_assets=$(echo "$all_assets" "$page_assets" | jq -s '.[0] + .[1]')
|
||||
|
||||
# Move to the next page
|
||||
assets_page=$((assets_page + 1))
|
||||
|
||||
# Check if we received fewer items than the per_page limit (meaning we're on the last page)
|
||||
if [ "$page_assets_count" -lt "$PER_PAGE" ]; then
|
||||
echo "Reached the last page of assets with $page_assets_count items"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Filter for UF2 and BIN files
|
||||
echo "Filtering for firmware files..."
|
||||
firmware_assets=$(echo "$all_assets" | jq '[.[] | select(.name | endswith(".uf2") or endswith(".bin"))]')
|
||||
|
||||
# Check if we found any assets
|
||||
asset_count=$(echo "$firmware_assets" | jq 'length')
|
||||
if [ "$asset_count" = "0" ]; then
|
||||
echo "No firmware files (.uf2 or .bin) found in release $tag_name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found $asset_count firmware files to download."
|
||||
|
||||
# Create a temporary directory for hash verification
|
||||
temp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$temp_dir"' EXIT
|
||||
|
||||
# Download each asset
|
||||
echo "$firmware_assets" | jq -r '.[] | @json' | while read -r asset_json; do
|
||||
name=$(echo "$asset_json" | jq -r '.name')
|
||||
url=$(echo "$asset_json" | jq -r '.browser_download_url')
|
||||
size=$(echo "$asset_json" | jq -r '.size')
|
||||
output_file="$OUTPUT_DIR/$name"
|
||||
|
||||
# Check if file already exists
|
||||
if [ -f "$output_file" ]; then
|
||||
echo "File $name already exists, checking if it's the same version..."
|
||||
|
||||
# Check file size
|
||||
existing_size=$(stat -c%s "$output_file" 2>/dev/null || stat -f%z "$output_file" 2>/dev/null)
|
||||
|
||||
if [ "$existing_size" = "$size" ]; then
|
||||
# Calculate hash of existing file
|
||||
existing_hash=$($SHA256_CMD "$output_file" | cut -d' ' -f1)
|
||||
|
||||
# Download to temp file for hash comparison
|
||||
temp_file="$temp_dir/$name"
|
||||
echo "Downloading $name to verify hash..."
|
||||
if curl -s -L -o "$temp_file" "$url"; then
|
||||
# Calculate hash of downloaded file
|
||||
remote_hash=$($SHA256_CMD "$temp_file" | cut -d' ' -f1)
|
||||
|
||||
# Compare hashes
|
||||
if [ "$existing_hash" = "$remote_hash" ]; then
|
||||
echo "File $name is already up to date (hash verified), skipping download."
|
||||
continue
|
||||
else
|
||||
echo "File $name has different hash, downloading new version..."
|
||||
fi
|
||||
else
|
||||
echo "Could not download $name for hash verification, will download again to be safe."
|
||||
fi
|
||||
else
|
||||
echo "File $name has different size, downloading new version..."
|
||||
fi
|
||||
else
|
||||
echo "Downloading $name..."
|
||||
fi
|
||||
|
||||
# Download the file
|
||||
if curl -L -o "$output_file" "$url"; then
|
||||
echo "Successfully downloaded $name"
|
||||
else
|
||||
echo "Failed to download $name"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Download complete. Firmware files saved to $OUTPUT_DIR/"
|
||||
ls -la "$OUTPUT_DIR"
|
||||
49
generate_fat_images.ps1
Normal file
49
generate_fat_images.ps1
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Generate FAT32 images for Wippersnapper firmware
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[int]$SizeMB
|
||||
)
|
||||
|
||||
# Get current timestamp
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
|
||||
|
||||
# Create a temporary file for the FAT image
|
||||
$imagePath = Join-Path -Path $OutputDir -ChildPath ("wippersnapper_fat_image_$timestamp.img")
|
||||
|
||||
# Create an empty file of the specified size
|
||||
$sizeBytes = $SizeMB * 1MB
|
||||
fsutil file createnew $imagePath $sizeBytes
|
||||
|
||||
# Create temporary diskpart script
|
||||
$scriptPath = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
# Create the script file content
|
||||
@"
|
||||
create vdisk file="$imagePath" maximum=$SizeMB type=fixed
|
||||
select vdisk file="$imagePath"
|
||||
attach vdisk
|
||||
create partition primary
|
||||
format fs=fat32 quick
|
||||
assign letter=X
|
||||
exit
|
||||
"@ | Out-File -FilePath $scriptPath -Encoding ascii
|
||||
|
||||
try {
|
||||
# Run diskpart with the script
|
||||
diskpart /s $scriptPath
|
||||
|
||||
# Wait for drive letter to be assigned
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# Return the image path
|
||||
return $imagePath
|
||||
} finally {
|
||||
# Clean up the script file
|
||||
Remove-Item $scriptPath -Force
|
||||
}
|
||||
1325
merge-script.py
Normal file
1325
merge-script.py
Normal file
File diff suppressed because it is too large
Load diff
634
offline.html
Normal file
634
offline.html
Normal file
|
|
@ -0,0 +1,634 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wippersnapper Configuration Builder</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #2e8b57;
|
||||
}
|
||||
.section {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
button {
|
||||
background-color: #2e8b57;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #3cb371;
|
||||
}
|
||||
select, input {
|
||||
padding: 8px;
|
||||
margin: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.component-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.component-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
width: 230px;
|
||||
background-color: white;
|
||||
}
|
||||
.component-card img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.selected {
|
||||
border: 2px solid #2e8b57;
|
||||
background-color: #f0fff0;
|
||||
}
|
||||
.pins-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
.pin {
|
||||
padding: 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
background-color: #f9f9f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pin.used {
|
||||
background-color: #ffdddd;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.pin.selected {
|
||||
background-color: #ddffdd;
|
||||
border-color: #2e8b57;
|
||||
}
|
||||
.config-output {
|
||||
background-color: #272822;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.i2c-bus-config {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tab {
|
||||
overflow: hidden;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.tab button {
|
||||
background-color: inherit;
|
||||
color: #333;
|
||||
float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 14px 16px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
.tab button.active {
|
||||
background-color: #2e8b57;
|
||||
color: white;
|
||||
}
|
||||
.tabcontent {
|
||||
display: none;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
.json-input {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.component-details-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.component-details-list li {
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.component-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.component-info {
|
||||
flex: 1;
|
||||
}
|
||||
.component-actions {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.loader {
|
||||
border: 8px solid #f3f3f3;
|
||||
border-top: 8px solid #2e8b57;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 2s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.board-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.board-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
margin-right: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
.board-details {
|
||||
flex: 1;
|
||||
}
|
||||
.search-filter {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.component-search {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1>Wippersnapper Configuration Builder</h1>
|
||||
<button id="reset-config-btn-top" class="reset-btn" style="background-color: #dc3545; margin-left: 20px;">Reset Configurator</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div id="loading-indicator" class="section">
|
||||
<h2>Loading Wippersnapper Data</h2>
|
||||
<div class="loader"></div>
|
||||
<p>Loading board and component definitions...</p>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<button class="tablinks active" onclick="openTab(event, 'BuildConfig')">Build Configuration</button>
|
||||
<button class="tablinks" onclick="openTab(event, 'ImportExport')">Import/Export</button>
|
||||
</div>
|
||||
|
||||
<div id="BuildConfig" class="tabcontent" style="display: block;">
|
||||
<div class="section">
|
||||
<h2>1. Select Board</h2>
|
||||
<select id="board-select">
|
||||
<option value="">-- Select a Board --</option>
|
||||
<!-- Board options will be dynamically populated -->
|
||||
</select>
|
||||
|
||||
<div id="board-details" class="hidden">
|
||||
<div class="board-preview">
|
||||
<img id="board-image" class="board-image hidden" src="" alt="Board image">
|
||||
<div class="board-details">
|
||||
<h3>Board Details</h3>
|
||||
<p><strong>Reference Voltage:</strong> <span id="ref-voltage">3.3</span>V</p>
|
||||
<p><strong>Total GPIO Pins:</strong> <span id="total-gpio">0</span></p>
|
||||
<p><strong>Total Analog Pins:</strong> <span id="total-analog">0</span></p>
|
||||
<p><strong>Default I2C Bus:</strong> SCL: <span id="default-scl">0</span>, SDA: <span id="default-sda">0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="companion-board-section" class="section hidden">
|
||||
<h2>2. Select Companion Board (Optional)</h2>
|
||||
<p>You can skip this step if you're not using a companion board.</p>
|
||||
<select id="companion-board-select">
|
||||
<option value="">-- None --</option>
|
||||
<option value="adalogger">Adafruit Adalogger FeatherWing</option>
|
||||
<option value="ds3231-precision">Adafruit DS3231 Precision RTC FeatherWing</option>
|
||||
<option value="picowbell-adalogger">Adafruit PiCowbell Adalogger for Pico</option>
|
||||
<option value="datalogger-shield-revb">Adafruit Data Logging Shield Rev.B (PCF8523)</option>
|
||||
<option value="datalogger-shield-reva">Adafruit Data Logging Shield Rev.A (DS1307)</option>
|
||||
<option value="audio-bff">Adafruit Audio BFF Add-on for QT Py and Xiao</option>
|
||||
<option value="microsd-bff">Adafruit microSD Card BFF Add-On for QT Py and Xiao</option>
|
||||
<option value="winc1500-shield">Adafruit WINC1500 WiFi Shield</option>
|
||||
<option value="airlift-shield">Adafruit AirLift Shield - ESP32 WiFi Co-Processor</option>
|
||||
|
||||
</select>
|
||||
|
||||
<div id="companion-details" class="hidden">
|
||||
<h3>Companion Board Details</h3>
|
||||
<p><strong>RTC:</strong> <span id="companion-rtc">None</span></p>
|
||||
<p><strong>SD Card CS Pin:</strong> <span id="companion-sd-cs">None</span></p>
|
||||
<p><strong>Additional Components:</strong> <span id="companion-extras">None</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="manual-config-section" class="section hidden">
|
||||
<h2>3. Manual Configuration</h2>
|
||||
|
||||
<div id="sd-card-config">
|
||||
<h3>SD Card Configuration</h3>
|
||||
<div id="sd-missing">
|
||||
<p>No SD card detected from companion board. Would you like to add an SD card? <b>(REQUIRED)</b></p>
|
||||
<label><input type="checkbox" id="add-sd-card"> Add SD Card</label>
|
||||
|
||||
<div id="sd-card-pin-select" class="hidden">
|
||||
<p>Select SD Card CS Pin:</p>
|
||||
<div id="sd-pins-list" class="pins-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sd-present" class="hidden">
|
||||
<p>SD Card CS Pin: <span id="sd-cs-pin"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rtc-config">
|
||||
<h3>RTC Configuration</h3>
|
||||
<div id="rtc-missing">
|
||||
<p>No RTC detected from companion board. Select RTC type:</p>
|
||||
<select id="rtc-select">
|
||||
<option value="soft">Software RTC</option>
|
||||
<option value="PCF8523">PCF8523</option>
|
||||
<option value="DS3231">DS3231</option>
|
||||
<option value="DS1307">DS1307</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="rtc-present" class="hidden">
|
||||
<p>RTC Type: <span id="rtc-type"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-led-config">
|
||||
<h3>Status LED Configuration</h3>
|
||||
<label for="led-brightness">Status LED Brightness (0.0-1.0): </label>
|
||||
<input type="range" id="led-brightness" min="0" max="1" step="0.1" value="0.5">
|
||||
<span id="brightness-value">0.5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="i2c-bus-section" class="section hidden">
|
||||
<h2>4. I2C Bus Configuration</h2>
|
||||
|
||||
<div id="default-i2c-bus" class="i2c-bus-config">
|
||||
<h3>Default I2C Bus</h3>
|
||||
<p>SCL: <span id="default-i2c-scl"></span>, SDA: <span id="default-i2c-sda"></span></p>
|
||||
</div>
|
||||
|
||||
<div id="additional-i2c-bus-container">
|
||||
<h3>Additional I2C Bus (Optional)</h3>
|
||||
<label><input type="checkbox" id="add-i2c-bus"> Add Additional I2C Bus</label>
|
||||
|
||||
<div id="additional-i2c-config" class="hidden i2c-bus-config">
|
||||
<p>Select SCL Pin:</p>
|
||||
<div id="scl-pins-list" class="pins-container"></div>
|
||||
<p>Select SDA Pin:</p>
|
||||
<div id="sda-pins-list" class="pins-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="i2c-mux-container">
|
||||
<h3>I2C Multiplexers (Optional)</h3>
|
||||
<button id="add-mux-btn">Add I2C Multiplexer</button>
|
||||
|
||||
<div id="mux-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="components-section" class="section hidden">
|
||||
<h2>5. Add Components</h2>
|
||||
|
||||
<div id="component-type-tabs" class="tab">
|
||||
<button class="comp-tab active" onclick="openComponentTab(event, 'all-components')">All Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'i2c-components')">I2C Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'ds18x20-components')">DS18x20 Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'pin-components')">Pin Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'pixel-components')">Pixel Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'pwm-components')">PWM Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'servo-components')">Servo Components</button>
|
||||
<button class="comp-tab" onclick="openComponentTab(event, 'uart-components')">UART Components</button>
|
||||
</div>
|
||||
|
||||
<div id="all-components" class="component-tabcontent" style="display: block;">
|
||||
<h3>All Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="all-search" class="component-search" placeholder="Search all components...">
|
||||
</div>
|
||||
<div class="component-list" id="all-component-list">
|
||||
<!-- All components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="i2c-components" class="component-tabcontent">
|
||||
<h3>I2C Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="i2c-search" class="component-search" placeholder="Search I2C components...">
|
||||
</div>
|
||||
<div>
|
||||
<label for="i2c-bus-select">Select I2C Bus: </label>
|
||||
<select id="i2c-bus-select">
|
||||
<option value="default">Default I2C Bus</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="component-list" id="i2c-component-list">
|
||||
<!-- I2C components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ds18x20-components" class="component-tabcontent">
|
||||
<h3>DS18x20 Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="ds18x20-search" class="component-search" placeholder="Search DS18x20 components...">
|
||||
</div>
|
||||
<div class="component-list" id="ds18x20-component-list">
|
||||
<!-- DS18x20 components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pin-components" class="component-tabcontent">
|
||||
<h3>Pin Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="pin-search" class="component-search" placeholder="Search Pin components...">
|
||||
</div>
|
||||
<div class="component-list" id="pin-component-list">
|
||||
<!-- Pin components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pixel-components" class="component-tabcontent">
|
||||
<h3>Pixel Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="pixel-search" class="component-search" placeholder="Search Pixel components...">
|
||||
</div>
|
||||
<div class="component-list" id="pixel-component-list">
|
||||
<!-- Pixel components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pwm-components" class="component-tabcontent">
|
||||
<h3>PWM Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="pwm-search" class="component-search" placeholder="Search PWM components...">
|
||||
</div>
|
||||
<div class="component-list" id="pwm-component-list">
|
||||
<!-- PWM components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="servo-components" class="component-tabcontent">
|
||||
<h3>Servo Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="servo-search" class="component-search" placeholder="Search Servo components...">
|
||||
</div>
|
||||
<div class="component-list" id="servo-component-list">
|
||||
<!-- Servo components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uart-components" class="component-tabcontent">
|
||||
<h3>UART Components</h3>
|
||||
<div class="search-filter">
|
||||
<input type="text" id="uart-search" class="component-search" placeholder="Search UART components...">
|
||||
</div>
|
||||
<div class="component-list" id="uart-component-list">
|
||||
<!-- UART components will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="selected-components-section" class="section hidden">
|
||||
<h2>6. Selected Components</h2>
|
||||
<div id="selected-components-list">
|
||||
<p>No components selected yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="generate-section" class="section hidden">
|
||||
<h2>7. Generate Configuration</h2>
|
||||
<button id="generate-config-btn">Generate Configuration</button>
|
||||
<div id="config-output-container" class="hidden">
|
||||
<h3>Configuration JSON:</h3>
|
||||
<pre id="config-output" class="config-output"></pre>
|
||||
<button id="download-config-btn">Download config.json</button>
|
||||
</div>
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<button id="reset-config-btn-bottom" class="reset-btn" style="background-color: #dc3545;">Reset Configurator</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ImportExport" class="tabcontent">
|
||||
<div class="section">
|
||||
<h2>Import Configuration</h2>
|
||||
<p>Import a previously saved configuration file:</p>
|
||||
<input type="file" id="import-file" accept=".json">
|
||||
<button id="import-btn">Import</button>
|
||||
|
||||
<p>Or paste your configuration JSON here:</p>
|
||||
<textarea id="import-json" class="json-input" placeholder="Paste your configuration JSON here..."></textarea>
|
||||
<button id="import-json-btn">Import from Text</button>
|
||||
|
||||
<div class="section">
|
||||
<h2>Export Configuration</h2>
|
||||
<p>Export the current configuration to a file:</p>
|
||||
<button id="export-btn">Export Configuration</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Add Custom Board</h2>
|
||||
<p>Add a custom board to the boards collection (for boards without definition.json files):</p>
|
||||
<div id="custom-board-form">
|
||||
<div>
|
||||
<label for="custom-board-name">Board Name:</label>
|
||||
<input type="text" id="custom-board-name" placeholder="e.g. My Custom Board">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-board-ref-voltage">Reference Voltage:</label>
|
||||
<input type="number" id="custom-board-ref-voltage" value="3.3" step="0.1" min="1.8" max="5.0">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-board-gpio">Total GPIO Pins:</label>
|
||||
<input type="number" id="custom-board-gpio" value="0" min="0" max="100">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-board-analog">Total Analog Pins:</label>
|
||||
<input type="number" id="custom-board-analog" value="0" min="0" max="100">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-board-scl">Default I2C SCL Pin:</label>
|
||||
<input type="text" id="custom-board-scl" placeholder="e.g. SCL, D3">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom-board-sda">Default I2C SDA Pin:</label>
|
||||
<input type="text" id="custom-board-sda" placeholder="e.g. SDA, D2">
|
||||
</div>
|
||||
<button id="add-custom-board-btn">Add Custom Board</button>
|
||||
</div>
|
||||
|
||||
<div id="custom-boards-list" class="hidden">
|
||||
<h3>Added Custom Boards:</h3>
|
||||
<ul id="custom-boards-items">
|
||||
<!-- Custom boards will be listed here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Component Configuration Modal -->
|
||||
<div id="component-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 1000;">
|
||||
<div style="background-color: white; margin: 10% auto; padding: 20px; width: 80%; max-width: 600px; border-radius: 5px;">
|
||||
<h2 id="modal-title">Configure Component</h2>
|
||||
<div id="modal-content">
|
||||
<!-- Dynamic content will be inserted here -->
|
||||
</div>
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<button id="modal-cancel">Cancel</button>
|
||||
<button id="modal-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load the data loader script -->
|
||||
<script src="load-wippersnapper-data.js"></script>
|
||||
|
||||
<!-- Load the original application script -->
|
||||
<script src="wippersnapper-config-builder.js"></script>
|
||||
|
||||
<script>
|
||||
// Initialize functions from the original script
|
||||
|
||||
// Tab Navigation functions
|
||||
function openTab(evt, tabName) {
|
||||
// Declare variables
|
||||
let i, tabcontent, tablinks;
|
||||
|
||||
// Get all elements with class="tabcontent" and hide them
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
// Get all elements with class="tablinks" and remove the class "active"
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
if (evt) {
|
||||
evt.currentTarget.className += " active";
|
||||
} else {
|
||||
// If called programmatically, find and activate the correct tab
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
if (tablinks[i].textContent.includes(tabName.replace('Export', 'Import/Export'))) {
|
||||
tablinks[i].className += " active";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openComponentTab(evt, tabName) {
|
||||
// Declare variables
|
||||
let i, tabcontent, tablinks;
|
||||
|
||||
// Get all elements with class="component-tabcontent" and hide them
|
||||
tabcontent = document.getElementsByClassName("component-tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
// Get all elements with class="comp-tab" and remove the class "active"
|
||||
tablinks = document.getElementsByClassName("comp-tab");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
if (evt) {
|
||||
evt.currentTarget.className += " active";
|
||||
} else {
|
||||
// If called programmatically, find and activate the correct tab
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
if (tablinks[i].textContent.includes(tabName.replace('-components', ''))) {
|
||||
tablinks[i].className += " active";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the application with data loading
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// This is handled by the load-wippersnapper-data.js script
|
||||
// It will call loadWippersnapperData() and initialize everything
|
||||
|
||||
// Make sure only All Components tab is visible initially
|
||||
const tabcontents = document.getElementsByClassName("component-tabcontent");
|
||||
for (let i = 0; i < tabcontents.length; i++) {
|
||||
if (tabcontents[i].id !== "all-components") {
|
||||
tabcontents[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Component search functionality
|
||||
document.querySelectorAll('.component-search').forEach(searchInput => {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const componentType = this.id.split('-')[0]; // Extract type from ID (e.g., "i2c" from "i2c-search")
|
||||
const componentList = document.getElementById(`${componentType}-component-list`);
|
||||
|
||||
// Filter components
|
||||
const components = componentList.querySelectorAll('.component-card');
|
||||
components.forEach(component => {
|
||||
const componentName = component.querySelector('h4').textContent.toLowerCase();
|
||||
const shouldShow = componentName.includes(searchTerm);
|
||||
component.style.display = shouldShow ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
129
offline.js
Normal file
129
offline.js
Normal file
File diff suppressed because one or more lines are too long
40
untested_product_image_api.py
Normal file
40
untested_product_image_api.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import requests
|
||||
import re
|
||||
|
||||
BASE_API = 'https://www.adafruit.com/api'
|
||||
|
||||
|
||||
def get_product_image(url):
|
||||
if '/product/' in url:
|
||||
product_id = re.search(r'/product/(\d+)', url)
|
||||
if product_id:
|
||||
product_id = product_id.group(1)
|
||||
api_url = f'{BASE_API}/product/{product_id}'
|
||||
response = requests.get(api_url)
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
return data.get('product_image')
|
||||
elif '/category/' in url:
|
||||
category_id = re.search(r'/category/(\d+)', url)
|
||||
if category_id:
|
||||
category_id = category_id.group(1)
|
||||
api_url = f'{BASE_API}/category/{category_id}'
|
||||
response = requests.get(api_url)
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
products = data.get('products')
|
||||
if products:
|
||||
return products[0].get('product_image')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Example usage:
|
||||
product_url = 'https://www.adafruit.com/product/998'
|
||||
category_url = 'https://www.adafruit.com/category/118'
|
||||
|
||||
product_image_url = get_product_image(product_url)
|
||||
print('Product Image:', product_image_url)
|
||||
|
||||
category_image_url = get_product_image(category_url)
|
||||
print('First Product Image from Category:', category_image_url)
|
||||
53
windows_fat32_create.ps1
Normal file
53
windows_fat32_create.ps1
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Create FAT32 image with specified size
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ImagePath,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[int]$SizeMB
|
||||
)
|
||||
|
||||
# Create an empty file of the specified size
|
||||
$sizeBytes = $SizeMB * 1MB
|
||||
fsutil file createnew $ImagePath $sizeBytes
|
||||
|
||||
# Create temporary diskpart script
|
||||
$scriptPath = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
# Create the script file content
|
||||
@"
|
||||
create vdisk file=$ImagePath maximum=$SizeMB type=fixed
|
||||
select vdisk file=$ImagePath
|
||||
attach vdisk
|
||||
create partition primary
|
||||
format fs=fat32 quick
|
||||
assign letter=X
|
||||
exit
|
||||
"@ | Out-File -FilePath $scriptPath -Encoding ascii
|
||||
|
||||
# Run diskpart with the script
|
||||
try {
|
||||
diskpart /s $scriptPath
|
||||
|
||||
# Wait for drive letter to be assigned
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# Now you can copy your files to X:
|
||||
# Copy-Item -Path "C:\path\to\your_files\*" -Destination "X:\" -Recurse
|
||||
|
||||
# Return the drive letter
|
||||
return "X:"
|
||||
} finally {
|
||||
# Detach the disk
|
||||
$detachScript = [System.IO.Path]::GetTempFileName()
|
||||
@"
|
||||
select vdisk file=$ImagePath
|
||||
detach vdisk
|
||||
exit
|
||||
"@ | Out-File -FilePath $detachScript -Encoding ascii
|
||||
|
||||
diskpart /s $detachScript
|
||||
Remove-Item $detachScript -Force
|
||||
# Clean up the script file
|
||||
Remove-Item $scriptPath -Force
|
||||
}
|
||||
268
windows_merge-script.py
Normal file
268
windows_merge-script.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import hashlib
|
||||
import json
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
# Constants
|
||||
BASE_DIR = Path(r"C:\dev\arduino\Adafruit_Wippersnapper_Offline_Configurator")
|
||||
OUTPUT_DIR = BASE_DIR / "merged_firmware"
|
||||
FIRMWARE_DIR = BASE_DIR / "latest_firmware"
|
||||
FAT_SIZE = 1024 * 1024 # 1MB FAT partition
|
||||
OFFLINE_FILES = ["offline.js", "offline.html"]
|
||||
|
||||
# Platform-specific parameters
|
||||
PLATFORM_PARAMS = {
|
||||
"RP2040": {
|
||||
"family": "RP2040",
|
||||
"fat_base_addr": 0x10100000, # Example, would need to be adjusted per target
|
||||
"uf2_family_id": "0xe48bff56" # RP2040 family ID for RP2040/RP2350/etc
|
||||
},
|
||||
"ESP32": {
|
||||
"family": "ESP32",
|
||||
"fat_base_addr": 0x310000, # Example, would need to be adjusted per target
|
||||
"partition_size": FAT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
# Map of board identifiers to detect specific target properties
|
||||
# This could be expanded based on the ci-arduino/all_platforms.py information
|
||||
BOARD_SPECIFIC_PARAMS = {
|
||||
# ESP32-S2 boards might have different partition layouts
|
||||
"esp32s2": {
|
||||
"platform": "ESP32",
|
||||
"fat_base_addr": 0x310000 # Adjust as needed
|
||||
},
|
||||
# ESP32-S3 boards
|
||||
"esp32s3": {
|
||||
"platform": "ESP32",
|
||||
"fat_base_addr": 0x310000 # Adjust as needed
|
||||
},
|
||||
# RP2350 might have specific settings
|
||||
"rp2350": {
|
||||
"platform": "RP2040",
|
||||
"fat_base_addr": 0x10100000 # Adjust as needed
|
||||
}
|
||||
# Add more board-specific configurations as needed
|
||||
}
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
def create_fat_image(output_path, files_to_include):
|
||||
"""Create a FAT32 image containing the specified files"""
|
||||
# Create a temporary directory for mounting
|
||||
with tempfile.TemporaryDirectory() as mount_dir:
|
||||
# Create a raw disk image
|
||||
img_size_bytes = FAT_SIZE
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(b'\x00' * img_size_bytes)
|
||||
|
||||
# Format as FAT32 using Windows diskpart
|
||||
diskpart_script = tempfile.NamedTemporaryFile(delete=False, suffix='.txt')
|
||||
try:
|
||||
# Get absolute paths
|
||||
abs_output_path = os.path.abspath(output_path)
|
||||
|
||||
# Write diskpart script
|
||||
with open(diskpart_script.name, 'w') as f:
|
||||
f.write(f"create vdisk file=\"{abs_output_path}\" maximum={img_size_bytes // 1024 // 1024} type=fixed\n")
|
||||
f.write(f"select vdisk file=\"{abs_output_path}\"\n")
|
||||
f.write("attach vdisk\n")
|
||||
f.write("create partition primary\n")
|
||||
f.write("format fs=fat32 quick\n")
|
||||
f.write("assign letter=X\n")
|
||||
f.write("exit\n")
|
||||
|
||||
# Run diskpart
|
||||
subprocess.run(["diskpart", "/s", diskpart_script.name], check=True)
|
||||
|
||||
# Copy files to the mounted drive
|
||||
for file_path in files_to_include:
|
||||
src_path = BASE_DIR / file_path
|
||||
dest_path = Path("X:/") / os.path.basename(file_path)
|
||||
shutil.copy2(src_path, dest_path)
|
||||
print(f"Copied {src_path} to {dest_path}")
|
||||
|
||||
# Detach the virtual disk
|
||||
detach_script = tempfile.NamedTemporaryFile(delete=False, suffix='.txt')
|
||||
with open(detach_script.name, 'w') as f:
|
||||
f.write(f"select vdisk file=\"{abs_output_path}\"\n")
|
||||
f.write("detach vdisk\n")
|
||||
f.write("exit\n")
|
||||
|
||||
subprocess.run(["diskpart", "/s", detach_script.name], check=True)
|
||||
os.unlink(detach_script.name)
|
||||
|
||||
finally:
|
||||
os.unlink(diskpart_script.name)
|
||||
|
||||
return output_path
|
||||
|
||||
def calculate_checksum(file_path):
|
||||
"""Calculate MD5 checksum of a file"""
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def convert_to_uf2(img_path, platform_type, output_path, base_addr):
|
||||
"""Convert raw image to UF2 format"""
|
||||
# Check if required tools are available
|
||||
if platform_type == "RP2040":
|
||||
# Using RP2040 UF2 converter
|
||||
uf2conv_script = os.environ.get("UF2CONV_PATH", "uf2conv.py") # You might need to set this env var
|
||||
cmd = [
|
||||
"python", uf2conv_script,
|
||||
"--base", hex(base_addr),
|
||||
"--family", PLATFORM_PARAMS[platform_type]["uf2_family_id"],
|
||||
"--output", output_path,
|
||||
img_path
|
||||
]
|
||||
else: # ESP32
|
||||
# Using TinyUF2 converter for ESP32
|
||||
tinyuf2_script = os.environ.get("TINYUF2_CONV_PATH", "tinyuf2/tools/uf2conv.py") # You might need to set this
|
||||
cmd = [
|
||||
"python", tinyuf2_script,
|
||||
"--base", hex(base_addr),
|
||||
"--family", platform_type,
|
||||
"--output", output_path,
|
||||
img_path
|
||||
]
|
||||
|
||||
print(f"Running command: {' '.join(cmd)}")
|
||||
subprocess.run(cmd, check=True)
|
||||
return output_path
|
||||
|
||||
def merge_uf2_files(fw_uf2, fs_uf2, output_path):
|
||||
"""Merge firmware and filesystem UF2 files"""
|
||||
# For simplicity we'll just concatenate the files
|
||||
# In a production environment, you might want a proper UF2 merger
|
||||
with open(output_path, 'wb') as outfile:
|
||||
for infile in [fw_uf2, fs_uf2]:
|
||||
with open(infile, 'rb') as f:
|
||||
outfile.write(f.read())
|
||||
|
||||
return output_path
|
||||
|
||||
def process_firmware_file(fw_path):
|
||||
"""Process a single firmware file, create merged version with filesystem"""
|
||||
fw_name = os.path.basename(fw_path)
|
||||
fw_checksum = calculate_checksum(fw_path)
|
||||
output_name = f"merged_{fw_checksum}_{fw_name}"
|
||||
output_path = OUTPUT_DIR / output_name
|
||||
|
||||
# Determine platform type based on filename
|
||||
if any(x in fw_name.lower() for x in ["esp32", "esp32s2", "esp32s3"]):
|
||||
platform_type = "ESP32"
|
||||
elif any(x in fw_name.lower() for x in ["rp2040", "rp2350", "rp2"]):
|
||||
platform_type = "RP2040"
|
||||
else:
|
||||
print(f"Warning: Could not determine platform type for {fw_name}, assuming RP2040")
|
||||
platform_type = "RP2040"
|
||||
|
||||
print(f"Processing {fw_name} as {platform_type} firmware...")
|
||||
|
||||
# Create temporary files
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create FAT image
|
||||
fat_img_path = os.path.join(temp_dir, "fat32.img")
|
||||
create_fat_image(fat_img_path, OFFLINE_FILES)
|
||||
|
||||
# Convert FAT image to UF2
|
||||
fs_uf2_path = os.path.join(temp_dir, "filesystem.uf2")
|
||||
base_addr = PLATFORM_PARAMS[platform_type]["fat_base_addr"]
|
||||
convert_to_uf2(fat_img_path, platform_type, fs_uf2_path, base_addr)
|
||||
|
||||
# Merge firmware UF2 with filesystem UF2
|
||||
merge_uf2_files(fw_path, fs_uf2_path, output_path)
|
||||
|
||||
print(f"Created merged firmware: {output_path}")
|
||||
return output_path
|
||||
|
||||
def fetch_ci_arduino_parameters():
|
||||
"""
|
||||
Attempt to fetch board-specific parameters from the ci-arduino repository
|
||||
This requires the repo to be locally cloned or accessible
|
||||
"""
|
||||
try:
|
||||
ci_arduino_path = os.environ.get("CI_ARDUINO_PATH", "../ci-arduino")
|
||||
platforms_file = Path(ci_arduino_path) / "all_platforms.py"
|
||||
|
||||
if not platforms_file.exists():
|
||||
print(f"Warning: Could not find ci-arduino platforms file at {platforms_file}")
|
||||
return {}
|
||||
|
||||
# Simple parsing of the platforms file to extract partition information
|
||||
# This is a basic approach - a more robust solution would use ast module
|
||||
# to properly parse the Python file
|
||||
board_params = {}
|
||||
with open(platforms_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Look for board definitions with partition information
|
||||
import re
|
||||
board_defs = re.findall(r'(\w+)\s*=\s*{(.*?)}', content, re.DOTALL)
|
||||
|
||||
for board_name, board_def in board_defs:
|
||||
# Look for partition information
|
||||
partition_match = re.search(r'["\'](build\.partitions)["\']:\s*["\'](.*?)["\']', board_def)
|
||||
if partition_match:
|
||||
partition_name = partition_match.group(2)
|
||||
# Add to our board parameters
|
||||
board_params[board_name.lower()] = {
|
||||
"partition_name": partition_name
|
||||
}
|
||||
print(f"Found partition info for {board_name}: {partition_name}")
|
||||
|
||||
return board_params
|
||||
except Exception as e:
|
||||
print(f"Error fetching ci-arduino parameters: {e}")
|
||||
return {}
|
||||
|
||||
def main():
|
||||
os.chdir(BASE_DIR)
|
||||
print(f"Working directory: {os.getcwd()}")
|
||||
|
||||
# Try to fetch board-specific parameters from ci-arduino repo
|
||||
ci_params = fetch_ci_arduino_parameters()
|
||||
if ci_params:
|
||||
print(f"Found {len(ci_params)} board configurations from ci-arduino")
|
||||
# Update our board parameters with the fetched information
|
||||
for board_id, params in ci_params.items():
|
||||
if board_id in BOARD_SPECIFIC_PARAMS:
|
||||
BOARD_SPECIFIC_PARAMS[board_id].update(params)
|
||||
else:
|
||||
# Determine platform type from board ID
|
||||
platform = "ESP32" if "esp" in board_id else "RP2040"
|
||||
BOARD_SPECIFIC_PARAMS[board_id] = {
|
||||
"platform": platform,
|
||||
"fat_base_addr": PLATFORM_PARAMS[platform]["fat_base_addr"],
|
||||
**params
|
||||
}
|
||||
|
||||
# Find all UF2 firmware files
|
||||
fw_files = list(FIRMWARE_DIR.glob("*.uf2"))
|
||||
if not fw_files:
|
||||
print(f"No UF2 firmware files found in {FIRMWARE_DIR}")
|
||||
return
|
||||
|
||||
print(f"Found {len(fw_files)} firmware files")
|
||||
|
||||
# Process each firmware file
|
||||
for fw_file in fw_files:
|
||||
try:
|
||||
process_firmware_file(fw_file)
|
||||
except Exception as e:
|
||||
print(f"Error processing {fw_file}: {e}")
|
||||
|
||||
print("Processing complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue