Merge branch 'use_boards_sd_card'

This commit is contained in:
Tyeth Gundry 2025-04-25 21:43:53 +01:00
commit 029c9bbf0e
17 changed files with 8451 additions and 9447 deletions

View file

@ -39,18 +39,26 @@ jobs:
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions@github.com"
git submodule update --remote --recursive
git submodule init
git submodule update --recursive
cd Wippersnapper_Boards
git reset --hard origin/rp2040_datalogger_feather
cd ../Wippersnapper_Components
git reset --hard origin/main
cd ..
git add Wippersnapper_Boards Wippersnapper_Components
git diff --staged --quiet || git commit -m "Update submodules to latest versions [skip ci]"
- name: Run conversion script
run: |
pip install requests beautifulsoup4
pip install requests
python convert_all_wippersnapper_definitions.py
- name: Commit changes if any
run: |
git add wippersnapper_boards.json wippersnapper_components.json
git add wippersnapper_boards.json wippersnapper_components.json wippersnapper_boards.js wippersnapper_components.js firmware-data.js
git diff --staged --quiet || git commit -m "Auto-update Wippersnapper definitions [skip ci]"
git push origin main
git push origin ${{ github.ref_name }}
- name: Setup Pages
uses: actions/configure-pages@v4

@ -1 +1 @@
Subproject commit 86fd50d32b66abe1fbf8c9a1926f310308dc5bea
Subproject commit 66d399578444320a039514f1a41160a0c2357212

@ -1 +1 @@
Subproject commit 848a7bc7fef4eb4701236ba2a0af4fb6f268e19d
Subproject commit 5b154e5389df0a9f10089057a15d5c2be82d6913

View file

@ -2,6 +2,7 @@ import os
import time
from convert_boards_to_json import convert_boards_to_json
from convert_components_to_json import convert_components_to_json
from fetch_latest_release_info_and_assets import main as fetch_latest_release_info_and_assets
def main():
"""
@ -19,6 +20,9 @@ def main():
# Convert components
print("\n--- Converting Components ---")
components = convert_components_to_json()
# fetch latest release info and assets
release_info = fetch_latest_release_info_and_assets()
# Print summary
elapsed_time = time.time() - start_time

View file

@ -2,6 +2,7 @@ import os
import json
import glob
from pathlib import Path
from convert_components_to_json import get_image_from_adafruit_product_url
# Base directory for the boards
BOARDS_DIR = r"./Wippersnapper_Boards/boards"
@ -11,57 +12,7 @@ def add_custom_board_definitions(boards):
"""
Add custom board definitions for boards that don't have definition.json files
"""
# Adafruit Feather RP2040 Adalogger
boards["feather-rp2040-adalogger"] = {
"boardName": "Adafruit Feather RP2040 Adalogger",
"mcuName": "RP2040",
"referenceVoltage": 3.3,
"displayName": "Adafruit Feather RP2040 Adalogger",
"vendor": "Adafruit",
"productURL": "https://www.adafruit.com/product/5980",
"documentationURL": "https://learn.adafruit.com/adafruit-feather-rp2040-adalogger",
"installMethod": "uf2",
"totalGPIOPins": 22,
"totalAnalogPins": 4,
"pins": [
{"number": 0, "name": "D0", "displayName": "GPIO0", "hasPWM": True, "hasServo": True},
{"number": 1, "name": "D1", "displayName": "GPIO1", "hasPWM": True, "hasServo": True},
{"number": 2, "name": "D2", "displayName": "GPIO2", "hasPWM": True, "hasServo": True},
{"number": 3, "name": "D3", "displayName": "GPIO3", "hasPWM": True, "hasServo": True},
{"number": 4, "name": "D4", "displayName": "GPIO4", "hasPWM": True, "hasServo": True},
{"number": 5, "name": "D5", "displayName": "GPIO5", "hasPWM": True, "hasServo": True},
{"number": 6, "name": "D6", "displayName": "GPIO6", "hasPWM": True, "hasServo": True},
{"number": 7, "name": "D7", "displayName": "GPIO7", "hasPWM": True, "hasServo": True},
{"number": 8, "name": "D8", "displayName": "GPIO8", "hasPWM": True, "hasServo": True},
{"number": 9, "name": "D9", "displayName": "GPIO9", "hasPWM": True, "hasServo": True},
{"number": 10, "name": "D10", "displayName": "GPIO10", "hasPWM": True, "hasServo": True},
{"number": 11, "name": "D11", "displayName": "GPIO11", "hasPWM": True, "hasServo": True},
{"number": 12, "name": "D12", "displayName": "GPIO12", "hasPWM": True, "hasServo": True},
{"number": 13, "name": "D13", "displayName": "GPIO13 (LED)", "hasPWM": True, "hasServo": True},
{"number": 16, "name": "D16", "displayName": "GPIO16 (MOSI)", "hasPWM": True, "hasServo": True},
{"number": 17, "name": "D17", "displayName": "GPIO17 (A1/SCK/SD CS)", "hasPWM": True, "hasServo": True},
{"number": 18, "name": "D18", "displayName": "GPIO18 (MISO)", "hasPWM": True, "hasServo": True},
{"number": 19, "name": "D19", "displayName": "GPIO19", "hasPWM": True, "hasServo": True},
{"number": 20, "name": "D20", "displayName": "GPIO20", "hasPWM": True, "hasServo": True},
{"number": 24, "name": "D24", "displayName": "GPIO24 (A0)", "hasPWM": True, "hasServo": True},
{"number": 25, "name": "D25", "displayName": "GPIO25 (A3)", "hasPWM": True, "hasServo": True},
{"number": 29, "name": "D29", "displayName": "GPIO29 (A2)", "hasPWM": True, "hasServo": True}
],
"analogPins": [
{"name": "A0", "displayName": "GPIO24 (A0)", "direction": "INPUT"},
{"name": "A1", "displayName": "GPIO17 (A1/SD CS)", "direction": "INPUT"},
{"name": "A2", "displayName": "GPIO29 (A2)", "direction": "INPUT"},
{"name": "A3", "displayName": "GPIO25 (A3)", "direction": "INPUT"}
],
"defaultI2C": {
"i2cPortId": 0,
"SCL": "D3",
"SDA": "D2"
},
"sdCardCS": 17, # GPIO17/A1 is the SD card CS pin
"rtcType": "PCF8523", # Built-in RTC chip
"image": None
}
# Generic ESP32-S2 based board
boards["generic-esp32-s2"] = {
"boardName": "Generic ESP32-S2",
@ -72,6 +23,9 @@ def add_custom_board_definitions(boards):
"productURL": "",
"documentationURL": "",
"installMethod": "uf2",
"installBoardName": "",
"rtc": None,
"sdCardCS": None,
"totalGPIOPins": 43,
"totalAnalogPins": 20,
"pins": [
@ -123,12 +77,12 @@ def add_custom_board_definitions(boards):
],
"defaultI2C": {
"i2cPortId": 0,
"SCL": "D9",
"SDA": "D8"
"SCL": 9,
"SDA": 8
},
"image": None
}
# Generic ESP32-S3 based board
boards["generic-esp32-s3"] = {
"boardName": "Generic ESP32-S3",
@ -139,6 +93,9 @@ def add_custom_board_definitions(boards):
"productURL": "",
"documentationURL": "",
"installMethod": "uf2",
"installBoardName": "",
"rtc": None,
"sdCardCS": None,
"totalGPIOPins": 48,
"totalAnalogPins": 20,
"pins": [
@ -192,12 +149,12 @@ def add_custom_board_definitions(boards):
],
"defaultI2C": {
"i2cPortId": 0,
"SCL": "D9",
"SDA": "D8"
"SCL": 9,
"SDA": 8
},
"image": None
}
# Generic RP2040 based board
boards["generic-rp2040"] = {
"boardName": "Generic RP2040",
@ -208,6 +165,9 @@ def add_custom_board_definitions(boards):
"productURL": "",
"documentationURL": "",
"installMethod": "uf2",
"installBoardName": "",
"rtc": None,
"sdCardCS": None,
"totalGPIOPins": 30,
"totalAnalogPins": 4,
"pins": [
@ -250,24 +210,27 @@ def add_custom_board_definitions(boards):
],
"defaultI2C": {
"i2cPortId": 0,
"SCL": "D3",
"SDA": "D2"
"SCL": 3,
"SDA": 2
},
"image": None
}
# Generic RP23xx based board
boards["generic-rp23xx"] = {
"boardName": "Generic RP23xx",
"mcuName": "RP23xx",
"referenceVoltage": 3.3,
"displayName": "Generic RP23xx Board",
"displayName": "Generic RP2350/RP2354 board",
"vendor": "Generic",
"productURL": "",
"documentationURL": "",
"installMethod": "uf2",
"totalGPIOPins": 30,
"totalAnalogPins": 4,
"installBoardName": "",
"rtc": None,
"sdCardCS": None,
"totalGPIOPins": 48,
"totalAnalogPins": 8,
"pins": [
{"number": 0, "name": "D0", "displayName": "GPIO0", "hasPWM": True, "hasServo": True},
{"number": 1, "name": "D1", "displayName": "GPIO1", "hasPWM": True, "hasServo": True},
@ -295,84 +258,49 @@ def add_custom_board_definitions(boards):
{"number": 23, "name": "D23", "displayName": "GPIO23", "hasPWM": True, "hasServo": True},
{"number": 24, "name": "D24", "displayName": "GPIO24", "hasPWM": True, "hasServo": True},
{"number": 25, "name": "D25", "displayName": "GPIO25", "hasPWM": True, "hasServo": True},
{"number": 26, "name": "D26", "displayName": "GPIO26", "hasPWM": True, "hasServo": True},
{"number": 27, "name": "D27", "displayName": "GPIO27", "hasPWM": True, "hasServo": True},
{"number": 28, "name": "D28", "displayName": "GPIO28", "hasPWM": True, "hasServo": True},
{"number": 29, "name": "D29", "displayName": "GPIO29", "hasPWM": True, "hasServo": True}
{"number": 26, "name": "A26", "displayName": "GPIO26 (ADC0)", "hasPWM": True, "hasServo": True},
{"number": 27, "name": "A27", "displayName": "GPIO27 (ADC1)", "hasPWM": True, "hasServo": True},
{"number": 28, "name": "A28", "displayName": "GPIO28 (ADC2)", "hasPWM": True, "hasServo": True},
{"number": 29, "name": "A29", "displayName": "GPIO29 (ADC3)", "hasPWM": True, "hasServo": True},
{"number": 30, "name": "D30", "displayName": "GPIO30", "hasPWM": True, "hasServo": True},
{"number": 31, "name": "D31", "displayName": "GPIO31", "hasPWM": True, "hasServo": True},
{"number": 32, "name": "D32", "displayName": "GPIO32", "hasPWM": True, "hasServo": True},
{"number": 33, "name": "D33", "displayName": "GPIO33", "hasPWM": True, "hasServo": True},
{"number": 34, "name": "D34", "displayName": "GPIO34", "hasPWM": True, "hasServo": True},
{"number": 35, "name": "D35", "displayName": "GPIO35", "hasPWM": True, "hasServo": True},
{"number": 36, "name": "D36", "displayName": "GPIO36", "hasPWM": True, "hasServo": True},
{"number": 37, "name": "D37", "displayName": "GPIO37", "hasPWM": True, "hasServo": True},
{"number": 38, "name": "D38", "displayName": "GPIO38", "hasPWM": True, "hasServo": True},
{"number": 39, "name": "D39", "displayName": "GPIO39", "hasPWM": True, "hasServo": True},
{"number": 40, "name": "A40", "displayName": "GPIO40 (ADC0)", "hasPWM": True, "hasServo": True},
{"number": 41, "name": "A41", "displayName": "GPIO41 (ADC1)", "hasPWM": True, "hasServo": True},
{"number": 42, "name": "A42", "displayName": "GPIO42 (ADC2)", "hasPWM": True, "hasServo": True},
{"number": 43, "name": "A43", "displayName": "GPIO43 (ADC3)", "hasPWM": True, "hasServo": True},
{"number": 44, "name": "D44", "displayName": "GPIO44", "hasPWM": True, "hasServo": True},
{"number": 45, "name": "D45", "displayName": "GPIO45", "hasPWM": True, "hasServo": True},
{"number": 46, "name": "D46", "displayName": "GPIO46", "hasPWM": True, "hasServo": True},
{"number": 47, "name": "D47", "displayName": "GPIO47", "hasPWM": True, "hasServo": True},
{"number": 48, "name": "D48", "displayName": "GPIO48", "hasPWM": True, "hasServo": True}
],
"analogPins": [
{"name": "A0", "displayName": "ADC0", "direction": "INPUT"},
{"name": "A1", "displayName": "ADC1", "direction": "INPUT"},
{"name": "A2", "displayName": "ADC2", "direction": "INPUT"},
{"name": "A3", "displayName": "ADC3", "direction": "INPUT"}
{"name": "A26", "displayName": "GPIO26 (ADC0)", "direction": "INPUT"},
{"name": "A27", "displayName": "GPIO27 (ADC1)", "direction": "INPUT"},
{"name": "A28", "displayName": "GPIO28 (ADC2)", "direction": "INPUT"},
{"name": "A29", "displayName": "GPIO29 (ADC3)", "direction": "INPUT"},
{"name": "A40", "displayName": "GPIO40 (ADC0)", "direction": "INPUT"},
{"name": "A41", "displayName": "GPIO41 (ADC1)", "direction": "INPUT"},
{"name": "A42", "displayName": "GPIO42 (ADC2)", "direction": "INPUT"},
{"name": "A43", "displayName": "GPIO43 (ADC3)", "direction": "INPUT"}
],
"defaultI2C": {
"i2cPortId": 0,
"SCL": "D3",
"SDA": "D2"
"SCL": 3,
"SDA": 2
},
"image": None
}
# Adafruit Metro RP2350 with SD card
boards["metro-rp2350-sd"] = {
"boardName": "Adafruit Metro RP2350",
"mcuName": "RP2350",
"referenceVoltage": 3.3,
"displayName": "Adafruit Metro RP2350 (with SD card)",
"vendor": "Adafruit",
"productURL": "https://www.adafruit.com/product/5786",
"documentationURL": "https://learn.adafruit.com/adafruit-metro-rp2350",
"installMethod": "uf2",
"totalGPIOPins": 30,
"totalAnalogPins": 4,
"pins": [
{"number": 0, "name": "D0", "displayName": "GPIO0", "hasPWM": True, "hasServo": True},
{"number": 1, "name": "D1", "displayName": "GPIO1", "hasPWM": True, "hasServo": True},
{"number": 2, "name": "D2", "displayName": "GPIO2", "hasPWM": True, "hasServo": True},
{"number": 3, "name": "D3", "displayName": "GPIO3", "hasPWM": True, "hasServo": True},
{"number": 4, "name": "D4", "displayName": "GPIO4", "hasPWM": True, "hasServo": True},
{"number": 5, "name": "D5", "displayName": "GPIO5", "hasPWM": True, "hasServo": True},
{"number": 6, "name": "D6", "displayName": "GPIO6", "hasPWM": True, "hasServo": True},
{"number": 7, "name": "D7", "displayName": "GPIO7", "hasPWM": True, "hasServo": True},
{"number": 8, "name": "D8", "displayName": "GPIO8", "hasPWM": True, "hasServo": True},
{"number": 9, "name": "D9", "displayName": "GPIO9", "hasPWM": True, "hasServo": True},
{"number": 10, "name": "D10", "displayName": "GPIO10 (SD CS)", "hasPWM": True, "hasServo": True},
{"number": 11, "name": "D11", "displayName": "GPIO11", "hasPWM": True, "hasServo": True},
{"number": 12, "name": "D12", "displayName": "GPIO12", "hasPWM": True, "hasServo": True},
{"number": 13, "name": "D13", "displayName": "GPIO13", "hasPWM": True, "hasServo": True},
{"number": 14, "name": "D14", "displayName": "GPIO14", "hasPWM": True, "hasServo": True},
{"number": 15, "name": "D15", "displayName": "GPIO15", "hasPWM": True, "hasServo": True},
{"number": 16, "name": "D16", "displayName": "GPIO16", "hasPWM": True, "hasServo": True},
{"number": 17, "name": "D17", "displayName": "GPIO17", "hasPWM": True, "hasServo": True},
{"number": 18, "name": "D18", "displayName": "GPIO18", "hasPWM": True, "hasServo": True},
{"number": 19, "name": "D19", "displayName": "GPIO19", "hasPWM": True, "hasServo": True},
{"number": 20, "name": "D20", "displayName": "GPIO20", "hasPWM": True, "hasServo": True},
{"number": 21, "name": "D21", "displayName": "GPIO21", "hasPWM": True, "hasServo": True},
{"number": 22, "name": "D22", "displayName": "GPIO22", "hasPWM": True, "hasServo": True},
{"number": 23, "name": "D23", "displayName": "GPIO23", "hasPWM": True, "hasServo": True},
{"number": 24, "name": "D24", "displayName": "GPIO24", "hasPWM": True, "hasServo": True},
{"number": 25, "name": "D25", "displayName": "GPIO25", "hasPWM": True, "hasServo": True},
{"number": 26, "name": "D26", "displayName": "GPIO26", "hasPWM": True, "hasServo": True},
{"number": 27, "name": "D27", "displayName": "GPIO27", "hasPWM": True, "hasServo": True},
{"number": 28, "name": "D28", "displayName": "GPIO28", "hasPWM": True, "hasServo": True},
{"number": 29, "name": "D29", "displayName": "GPIO29", "hasPWM": True, "hasServo": True}
],
"analogPins": [
{"name": "A0", "displayName": "ADC0", "direction": "INPUT"},
{"name": "A1", "displayName": "ADC1", "direction": "INPUT"},
{"name": "A2", "displayName": "ADC2", "direction": "INPUT"},
{"name": "A3", "displayName": "ADC3", "direction": "INPUT"}
],
"defaultI2C": {
"i2cPortId": 0,
"SCL": "D3",
"SDA": "D2"
},
"sdCardCS": 10,
"image": None
}
return boards
def convert_boards_to_json():
@ -381,24 +309,24 @@ def convert_boards_to_json():
that can be used in the Wippersnapper Configuration Builder.
"""
boards = {}
# Get all board directories
board_dirs = [d for d in os.listdir(BOARDS_DIR) if os.path.isdir(os.path.join(BOARDS_DIR, d)) and not d.startswith('.')]
for board_dir in board_dirs:
board_path = os.path.join(BOARDS_DIR, board_dir)
definition_file = os.path.join(board_path, "definition.json")
# Skip if the definition.json doesn't exist
if not os.path.exists(definition_file):
print(f"Skipping {board_dir} - No definition.json found")
continue
try:
# Read the definition.json file
with open(definition_file, 'r') as f:
board_data = json.load(f)
# Extract relevant information
board_info = {
"boardName": board_data.get("boardName"),
@ -409,18 +337,28 @@ def convert_boards_to_json():
"productURL": board_data.get("productURL", ""),
"documentationURL": board_data.get("documentationURL", ""),
"installMethod": board_data.get("installMethod", ""),
"installBoardName": board_data.get("installBoardName", board_data.get("boardName", "")),
"rtc": board_data.get("rtc", None),
"sdCardCS": board_data.get("sdCardCS", None),
"pins": [],
"analogPins": [],
"defaultI2C": {},
"image": None
}
SUPPORTED_ARCHITECTURES = ["rp2350", "rp2040", "esp32s2", "esp32s3"]
# Filter out boards that are not supported
if board_info["mcuName"] not in SUPPORTED_ARCHITECTURES:
print(f"Skipping {board_dir} - Unsupported architecture {board_info['mcuName']}")
continue
# Get pins
if "components" in board_data and "digitalPins" in board_data["components"]:
for pin in board_data["components"]["digitalPins"]:
# Extract the numeric part of the pin name (e.g., "D13" -> 13)
pin_name = pin.get("name", "")
if pin_name.startswith("D"):
# if board_info["pins"].filter(lambda x: x["name"] == pin_name) == 0:
try:
pin_number = int(pin_name[1:])
board_info["pins"].append({
@ -432,20 +370,25 @@ def convert_boards_to_json():
})
except ValueError:
print(f"Skipping pin {pin_name} - Cannot parse pin number")
# Get analog pins
if "components" in board_data and "analogPins" in board_data["components"]:
analog_count = 0
for pin in board_data["components"]["analogPins"]:
pin_name = pin.get("name", "")
board_info["analogPins"].append({
"name": pin_name,
"displayName": pin.get("displayName", pin_name),
"direction": pin.get("direction", "INPUT")
})
analog_count += 1
try:
pin_number = int(pin_name[1:])
board_info["analogPins"].append({
"number": pin_number,
"name": pin_name,
"displayName": pin.get("displayName", pin_name),
"direction": pin.get("direction", "INPUT")
})
analog_count += 1
except ValueError:
print(f"Skipping pin {pin_name} - Cannot parse pin number")
board_info["totalAnalogPins"] = analog_count
# Get I2C ports
if "components" in board_data and "i2cPorts" in board_data["components"]:
i2c_ports = board_data["components"]["i2cPorts"]
@ -456,13 +399,13 @@ def convert_boards_to_json():
"SCL": default_i2c.get("SCL"),
"SDA": default_i2c.get("SDA")
}
# Store all I2C ports
board_info["i2cPorts"] = i2c_ports
# Count total GPIO pins
board_info["totalGPIOPins"] = len(board_info["pins"])
# Look for an image file
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg']
for ext in image_extensions:
@ -471,27 +414,31 @@ def convert_boards_to_json():
# Store relative path to image
board_info["image"] = f"boards/{board_dir}/image{ext}"
break
# Fetch missing images
if board_info["image"] is None and board_info["productURL"] != "":
board_info["image"] = get_image_from_adafruit_product_url(board_info["productURL"])
# Add to boards dictionary
boards[board_dir] = board_info
print(f"Processed {board_dir}")
except Exception as e:
print(f"Error processing {board_dir}: {str(e)}")
# Add custom board definitions
boards = add_custom_board_definitions(boards)
# Write the consolidated JSON file
with open(OUTPUT_FILE, 'w') as f:
json.dump({"boards": boards}, f, indent=2)
# Write the consolidated JS file
with open(OUTPUT_FILE.replace('.json', '.js'), 'w') as f:
f.write("window.jsonBoardObject = ")
json.dump({"boards": boards}, f, indent=2)
f.write(";\n")
print(f"Successfully created {OUTPUT_FILE} with {len(boards)} boards")
return boards

View file

@ -3,7 +3,7 @@ import json
import glob
import re
import requests
from bs4 import BeautifulSoup
# from bs4 import BeautifulSoup
from pathlib import Path
# Base directory for the components
@ -12,8 +12,8 @@ OUTPUT_FILE = r"./wippersnapper_components.json"
def get_image_from_adafruit_product_url(product_url):
"""
Fetch the product image URL from an Adafruit product page by extracting
the og:image meta tag from the HTML.
Fetch the product image URL from an Adafruit product API
(was from prod page by extracting the og:image meta tag from the HTML).
Args:
product_url (str): URL to an Adafruit product page
@ -21,28 +21,43 @@ def get_image_from_adafruit_product_url(product_url):
Returns:
str or None: URL to the product image, or None if not found
"""
if not product_url or not re.match(r'https?://(?:www\.)?adafruit\.com/product/\d+', product_url):
if not product_url or not re.match(r'https?://(?:www\.)?adafruit\.com/(?:product|category)/\d+', product_url):
print(f"Invalid Adafruit product URL ({product_url}) provided. Skipping image fetch.")
return None
# Grab product JSON from https://www.adafruit.com/api/products, cache, save as ISO date string filename so can be easily used as cache key
try:
response = requests.get(product_url, timeout=10)
product_id = re.search(r'/product/(\d+)', product_url)
category = re.search(r'/category/(\d+)', product_url)
url_to_fetch = f"https://www.adafruit.com/api/{("category" if category else "product")}/{(product_id.groups(1)[0] if product_id else category.groups(1)[0])}"
print(f"Fetching image from Adafruit API for {product_url}...\nGET {url_to_fetch}")
response = requests.get(url_to_fetch, timeout=10)
if response.status_code != 200:
print(f"Failed to fetch product page: {product_url}, status code: {response.status_code}")
print(f"Failed to fetch product data: {product_url}, status code: {response.status_code}")
return None
response_json = response.json()
if (response_json is None or response_json == [] or response_json == {} or 'error' in response_json):
print(f"Invalid response from API for {product_url}: {response_json}")
return None
soup = BeautifulSoup(response.text, 'html.parser')
og_image = soup.find('meta', property='og:image')
if og_image and 'content' in og_image.attrs:
image_url = og_image['content']
print(f"Found image URL from product page: {image_url}")
if 'product_image' in response_json:
image_url = response_json['product_image']
print(f"Found image URL from API: {image_url}")
return image_url
else:
print(f"No og:image meta tag found for: {product_url}")
return None
except Exception as e:
print(f"Error fetching image from product URL {product_url}: {str(e)}")
elif 'subcategories' in response_json:
subcategories = response_json['subcategories']
for subcategory in subcategories:
if 'product_image' in subcategory:
image_url = subcategory['product_image']
print(f"Found image URL from API: {image_url}")
return image_url
print(f"No image found in subcategory for: {product_url}")
return None
except Exception as e:
print(f"Error fetching image from API for {product_url}: {str(e)}")
return None
## Consider removing beautifulsoup...
def map_datatypes_to_offline_types(datatype):
"""
@ -84,19 +99,29 @@ def convert_components_to_json():
with open(definition_file, 'r') as f:
component_data = json.load(f)
# Skip non-input components for now
if category == "pin":
if component_data.get("direction", "").lower() != "input":
continue
# Extract relevant information
component_info = {
"id": component_dir,
"displayName": component_data.get("displayName", component_dir),
"name": component_data.get("name", component_dir),
"description": component_data.get("description", ""),
"category": category,
"dataTypes": [],
"image": None
}
# Store product URL if available
if "productURL" in component_data:
component_info["productUrl"] = component_data["productURL"]
# store documentation URL if available
if "documentationURL" in component_data:
component_info["documentationUrl"] = component_data["documentationURL"]
# Extract data types if available
if "subcomponents" in component_data:
@ -109,6 +134,10 @@ def convert_components_to_json():
else:
component_info["dataTypes"].append(map_datatypes_to_offline_types(meas_type))
# Handle pin category (MODE becomes componentAPI value)
if category == "pin":
component_info["componentAPI"] = component_data.get("mode", "digital").lower() + "io"
# Handle I2C-specific properties
if category == "i2c":
# Extract I2C address from parameters

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View file

@ -0,0 +1,212 @@
# Take releases for wippersnapper, filtered to semver including -offline, get next page until finished if none.
# Later match board name to installBoardName from boards (which falls back to boardName)
import requests
import json
import os
from datetime import datetime
# Configuration
REPO = "adafruit/Adafruit_Wippersnapper_Arduino"
OUTPUT_DIR = "latest_firmware"
RELEASES_API_URL = f"https://api.github.com/repos/{REPO}/releases"
PER_PAGE = 30 # GitHub API default per page limit
JS_OUTPUT_FILE = "firmware-data.js"
def fetch_latest_offline_release():
"""Fetch the latest offline release from GitHub API."""
print(f"Fetching releases information for {REPO}...")
headers = {
"Accept": "application/vnd.github.v3+json"
}
all_releases = []
page = 1
found_offline_release = False
latest_offline_release = None
# Loop through pages until we find an offline release or run out of releases
while not found_offline_release:
page_url = f"{RELEASES_API_URL}?page={page}&per_page={PER_PAGE}"
print(f"Fetching page {page} of releases...")
response = requests.get(page_url, headers=headers)
response.raise_for_status()
page_releases = response.json()
all_releases.extend(page_releases)
# Check if this page contains any offline releases
offline_releases = [r for r in page_releases if "offline" in r.get("tag_name", "")]
offline_releases_count = len(offline_releases)
print(f"Found {offline_releases_count} offline releases on page {page}")
if offline_releases_count > 0:
found_offline_release = True
# Get the latest offline release by published date
latest_offline_release = sorted(
offline_releases,
key=lambda x: x.get("published_at", ""),
reverse=True
)[0]
break
# Move to next page if we didn't find any offline releases and there are more pages
page += 1
# Stop if we received fewer items than the per_page limit (meaning we're on the last page)
if len(page_releases) < PER_PAGE:
print(f"Reached the last page with {len(page_releases)} items (less than {PER_PAGE} per page)")
break
# Check if we found an offline release
if latest_offline_release is None:
raise Exception(f"No offline release found after checking {page} pages of releases.")
tag_name = latest_offline_release.get("tag_name")
release_name = latest_offline_release.get("name")
release_id = latest_offline_release.get("id")
release_html_url = latest_offline_release.get("html_url")
release_published_date = latest_offline_release.get("published_at")
print(f"Found latest offline release: {release_name} (Tag: {tag_name}, ID: {release_id})")
return latest_offline_release, release_id, tag_name, release_name, release_html_url, release_published_date
def fetch_release_assets(release_id):
"""Fetch all assets for a specific release."""
assets_url = f"https://api.github.com/repos/{REPO}/releases/{release_id}/assets"
print(f"Fetching complete assets list from: {assets_url}")
headers = {
"Accept": "application/vnd.github.v3+json"
}
all_assets = []
assets_page = 1
# Loop through pages to get all assets
while True:
assets_page_url = f"{assets_url}?page={assets_page}&per_page={PER_PAGE}"
print(f"Fetching page {assets_page} of assets...")
response = requests.get(assets_page_url, headers=headers)
response.raise_for_status()
page_assets = response.json()
# Add to all assets - ensure it's a list
if isinstance(page_assets, list):
all_assets.extend(page_assets)
else:
all_assets.append(page_assets)
# Move to next page if there are more assets
assets_page += 1
# Stop if we received fewer items than the per_page limit (meaning we're on the last page)
if len(page_assets) < PER_PAGE:
print(f"Reached the last page of assets with {len(page_assets)} items")
break
# Filter for UF2 files
print("Filtering for firmware files...")
firmware_assets = [
asset for asset in all_assets
if asset.get("name", "").endswith(".uf2")# or asset.get("name", "").endswith(".bin")
]
# Check if we found any assets
if len(firmware_assets) == 0:
print(f"No firmware files (.uf2) found in this release.")
return firmware_assets
def generate_js_data(release_info, assets):
"""Generate JavaScript data structure with firmware information."""
tag_name, release_name, release_html_url, release_published_date = release_info
# Format the published date
try:
dt = datetime.fromisoformat(release_published_date.replace("Z", "+00:00"))
formatted_date = dt.strftime("%Y-%m-%d")
except:
formatted_date = release_published_date
# Create firmware data object
firmware_data = {
"releaseInfo": {
"version": tag_name,
"name": release_name,
"url": release_html_url,
"publishedDate": formatted_date
},
"firmwareFiles": []
}
# Add firmware files info
for asset in assets:
if asset.get("name", "").endswith(".uf2") or asset.get("name", "").endswith(".bin"):
firmware_data["firmwareFiles"].append({
"name": asset.get("name"),
"url": asset.get("browser_download_url"),
"size": asset.get("size"),
"downloadCount": asset.get("download_count", 0),
"contentType": asset.get("content_type"),
"createdAt": asset.get("created_at")
})
return firmware_data
def save_js_data(firmware_data):
"""Save firmware data as a JavaScript file."""
js_content = f"""// Auto-generated on {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
const FIRMWARE_DATA = {json.dumps(firmware_data, indent=2)};
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {{
module.exports = {{ FIRMWARE_DATA }};
}} else {{
window.FIRMWARE_DATA = FIRMWARE_DATA;
}}
"""
with open(JS_OUTPUT_FILE, "w") as f:
f.write(js_content)
print(f"JavaScript data saved to {JS_OUTPUT_FILE}")
def main():
try:
# Create output directory if it doesn't exist
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
print(f"Created directory: {OUTPUT_DIR}")
# Fetch latest offline release
release_data, release_id, tag_name, release_name, release_html_url, release_published_date = fetch_latest_offline_release()
# Fetch assets for this release
firmware_assets = fetch_release_assets(release_id)
print(f"Found {len(firmware_assets)} firmware files.")
# Generate JavaScript data
firmware_data = generate_js_data(
(tag_name, release_name, release_html_url, release_published_date),
firmware_assets
)
# Save as JavaScript file
save_js_data(firmware_data)
print("Process completed successfully.")
except Exception as e:
print(f"Error: {str(e)}")
if __name__ == "__main__":
main()

146
firmware-data.js Normal file
View file

@ -0,0 +1,146 @@
// Auto-generated on 2025-04-25 17:55:57
const FIRMWARE_DATA = {
"releaseInfo": {
"version": "1.0.0-offline-beta.2",
"name": "WipperSnapper Offline 1.0.0-offline-beta.2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/tag/1.0.0-offline-beta.2",
"publishedDate": "2025-04-14"
},
"firmwareFiles": [
{
"name": "wippersnapper.feather_esp32s2.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s2.1.0.0-offline-beta.2.uf2",
"size": 2735616,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_esp32s2_reverse_tft.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s2_reverse_tft.1.0.0-offline-beta.2.uf2",
"size": 2735616,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_esp32s2_tft.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s2_tft.1.0.0-offline-beta.2.uf2",
"size": 2735616,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_esp32s3.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s3.1.0.0-offline-beta.2.uf2",
"size": 2792960,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_esp32s3_4mbflash_2mbpsram.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s3_4mbflash_2mbpsram.1.0.0-offline-beta.2.uf2",
"size": 2798592,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_esp32s3_reverse_tft.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s3_reverse_tft.1.0.0-offline-beta.2.uf2",
"size": 2798592,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_esp32s3_tft.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_esp32s3_tft.1.0.0-offline-beta.2.uf2",
"size": 2798592,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.feather_rp2040_adalogger_tinyusb.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.feather_rp2040_adalogger_tinyusb.1.0.0-offline-beta.2.uf2",
"size": 648192,
"downloadCount": 1,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.metroesp32s2.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.metroesp32s2.1.0.0-offline-beta.2.uf2",
"size": 2735616,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.metro_esp32s3.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.metro_esp32s3.1.0.0-offline-beta.2.uf2",
"size": 2802688,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.metro_rp2350_tinyusb.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.metro_rp2350_tinyusb.1.0.0-offline-beta.2.uf2",
"size": 653824,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.pico_rp2040_tinyusb.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.pico_rp2040_tinyusb.1.0.0-offline-beta.2.uf2",
"size": 646656,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.pico_rp2350_tinyusb.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.pico_rp2350_tinyusb.1.0.0-offline-beta.2.uf2",
"size": 638464,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.qtpy_esp32s2.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.qtpy_esp32s2.1.0.0-offline-beta.2.uf2",
"size": 2735616,
"downloadCount": 1,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.qtpy_esp32s3.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.qtpy_esp32s3.1.0.0-offline-beta.2.uf2",
"size": 2792960,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
},
{
"name": "wippersnapper.qtpy_esp32s3_n4r2.1.0.0-offline-beta.2.uf2",
"url": "https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases/download/1.0.0-offline-beta.2/wippersnapper.qtpy_esp32s3_n4r2.1.0.0-offline-beta.2.uf2",
"size": 2798592,
"downloadCount": 0,
"contentType": "application/octet-stream",
"createdAt": "2025-04-14T16:09:53Z"
}
]
};
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { FIRMWARE_DATA };
} else {
window.FIRMWARE_DATA = FIRMWARE_DATA;
}

View file

@ -3,8 +3,20 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<title>Wippersnapper Configuration Builder</title>
<style>
.not-available-yet {
text-decoration: line-through;
font-size: x-small;
}
.component-tabcontent > h3 {
display: inline-block;
padding-right: 20px;
}
.component-tabcontent > .search-filter {
display: inline-block;
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
@ -45,6 +57,9 @@
display: flex;
flex-wrap: wrap;
gap: 10px;
scrollbar-width: auto;
max-height: 80vh;
overflow: scroll;
}
.component-card {
border: 1px solid #ddd;
@ -53,6 +68,12 @@
width: 230px;
background-color: white;
}
.component-card h4 {
margin: 0 0 10px 0;
}
.component-card p {
margin: 0 0 10px 0;
}
.component-card img {
max-width: 100%;
height: auto;
@ -94,11 +115,26 @@
.hidden {
display: none;
}
.i2c-bus-config {
.inline-row {
display: flex;
align-items: center;
align-content:space-around;
margin-bottom: 10px;
}
.dashed-section {
margin: 10px 0;
padding: 10px;
border: 1px dashed #ccc;
border-radius: 4px;
}
.dashed-section h3{
padding-top: 0px;
margin-top:0px;
}
.dashed-section > :last-child {
padding-bottom: 0px;
margin-bottom: 0px;
}
.tab {
overflow: hidden;
@ -184,6 +220,9 @@
.board-details {
flex: 1;
}
.board-details > p {
margin: 5px 0 0 0;
}
.search-filter {
margin-bottom: 15px;
}
@ -195,26 +234,36 @@
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 style="flex: 1;">
<h1>Wippersnapper Offline <br/>Data Logger Configurator</h1>
</div>
<div style="flex: 1; text-align: right;">
<div style="display:inline-block; font-size: smaller;"><b>Latest Release:</b> <a id="release_name" href="https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases">Release Page</a></div>
<div style="display:inline-block; margin: 5px; font-size: smaller;">Download: &nbsp;<a style="display: inline-flex;" id="firmware_file" href="https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/releases">UF2 firmware</a> </div>
</div>
<div style="flex: 1; text-align: right;">
<button id="reset-config-btn-top" class="reset-btn" style="background-color: #dc3545; margin-left: 20px;">Reset Configurator</button>
</div>
</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>
<button class="tablinks" onclick="openTab(event, 'ImportExport')">Import Config / Custom Board</button>
</div>
<div id="BuildConfig" class="tabcontent" style="display: block;">
<div class="section">
<h2>1. Select Board</h2>
@ -222,26 +271,27 @@
<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>
<h3>Board Details <span id="board-purchase"></span> <span id="board-help"></span></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>
<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>
<p>You can skip this step if you're not using a companion board.
<br/><i><small>It's just a convenient way to prepopulate the Real-time clock model and SD Card's Chip Select pin option.</small></i></p>
<select id="companion-board-select">
<option value="">-- None --</option>
<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>
@ -253,26 +303,31 @@
<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 class="board-preview">
<img id="companion-image" class="board-image hidden" src="#" alt="Companion board image">
<div class="board-details">
<h3>Companion Board Details <span id="companion-purchase"></span> <span id="companion-help"></span></h3>
<p><strong>Real-time clock (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>
</div>
<div id="manual-config-section" class="section hidden">
<h2>3. Manual Configuration</h2>
<div id="sd-card-config">
<div id="sd-card-config" class="dashed-section">
<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>
<p>Select SD Card CS Pin: <span id="manual-sd-cs-pin"></span></p>
<div id="sd-pins-list" class="pins-container"></div>
</div>
</div>
@ -280,12 +335,14 @@
<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-config" class="dashed-section">
<h3>Real-time clock (RTC) Configuration</h3>
<p style="color: red; font-weight: bold;">Note: The RTC must be on the first (default) I2C bus.</p>
<div id="rtc-missing">
<p>No RTC detected from companion board. Select RTC type:</p>
<select id="rtc-select">
<p>No RTC detected from companion board.</p>
<label for="rtc-select">Select RTC type: </label>
<select id="rtc-select">
<option value="soft">Software RTC</option>
<option value="PCF8523">PCF8523</option>
<option value="DS3231">DS3231</option>
@ -296,57 +353,60 @@
<p>RTC Type: <span id="rtc-type"></span></p>
</div>
</div>
<div id="status-led-config">
<div id="status-led-config" class="dashed-section">
<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 class="inline-row">
<label for="led-brightness">Status LED Brightness (0.0-1.0): </label>
<input type="range" id="led-brightness" min="0" max="1" step="0.01" value="0.5">
<span id="brightness-value">0.5</span>
</div>
</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">
<div id="default-i2c-bus" class="dashed-section">
<h3>Default I2C Bus</h3>
<p>SCL: <span id="default-i2c-scl"></span>, SDA: <span id="default-i2c-sda"></span></p>
<p>SCL: <span id="default-i2c-SCL"></span>, SDA: <span id="default-i2c-SDA"></span></p>
</div>
<div id="additional-i2c-bus-container">
<div id="additional-i2c-bus-container" class="dashed-section">
<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 id="additional-i2c-config" class="hidden dashed-section">
<p>Select SCL Pin: <span id="alt-SCL-pin"></span></p>
<div id="SCL-pins-list" class="pins-container"></div>
<p>Select SDA Pin: <span id="alt-SDA-pin"></span></p>
<div id="SDA-pins-list" class="pins-container"></div>
</div>
</div>
<div id="i2c-mux-container">
<div id="i2c-mux-container" class="dashed-section">
<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>
<p style="color: red;"><small><i>UART + other component categories are currently being implemented in offline mode (No ETA, but maybe reactive actions too!).</i></small></p>
<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>
<button class="comp-tab not-available-yet" onclick="openComponentTab(event, 'pixel-components')">Pixel Components</button>
<button class="comp-tab not-available-yet" onclick="openComponentTab(event, 'pwm-components')">PWM Components</button>
<button class="comp-tab not-available-yet" onclick="openComponentTab(event, 'servo-components')">Servo Components</button>
<button class="comp-tab not-available-yet" 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">
@ -356,24 +416,18 @@
<!-- 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">
@ -383,7 +437,7 @@
<!-- DS18x20 components will be populated here -->
</div>
</div>
<div id="pin-components" class="component-tabcontent">
<h3>Pin Components</h3>
<div class="search-filter">
@ -393,7 +447,7 @@
<!-- Pin components will be populated here -->
</div>
</div>
<div id="pixel-components" class="component-tabcontent">
<h3>Pixel Components</h3>
<div class="search-filter">
@ -403,7 +457,7 @@
<!-- Pixel components will be populated here -->
</div>
</div>
<div id="pwm-components" class="component-tabcontent">
<h3>PWM Components</h3>
<div class="search-filter">
@ -413,7 +467,7 @@
<!-- PWM components will be populated here -->
</div>
</div>
<div id="servo-components" class="component-tabcontent">
<h3>Servo Components</h3>
<div class="search-filter">
@ -423,7 +477,7 @@
<!-- Servo components will be populated here -->
</div>
</div>
<div id="uart-components" class="component-tabcontent">
<h3>UART Components</h3>
<div class="search-filter">
@ -434,17 +488,18 @@
</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>
<input type="checkbox" id="use-auto-init" onchange="javascript:appState.enableautoConfig=this.checked;"> <label for="use-auto-init" title="Auto config fallback for I2C sensors that fail to initialise (selects alternative sensors at same address)">Use Auto Init fallback</label>
<div id="config-output-container" class="hidden">
<h3>Configuration JSON:</h3>
<pre id="config-output" class="config-output"></pre>
@ -455,24 +510,24 @@
</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">
<div class="section" style="visibility: hidden; height: 0px;">
<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>
@ -494,16 +549,16 @@
<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">
<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">
<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">
@ -527,33 +582,35 @@
</div>
</div>
</div>
<!-- Load release info -->
<script src="firmware-data.js"></script>
<!-- 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) {
@ -571,19 +628,19 @@
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) {
@ -597,12 +654,12 @@
}
}
}
// 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++) {
@ -611,14 +668,14 @@
}
}
});
// 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 => {

View file

@ -1,8 +1,8 @@
// Load Wippersnapper boards and components data
// Configuration
const BOARDS_JSON_URL = 'https://raw.githubusercontent.com/tyeth/Adafruit_Wippersnapper_Offline_Configurator/refs/heads/main/wippersnapper_boards.json'; //'wippersnapper_boards.json';
const COMPONENTS_JSON_URL = 'https://raw.githubusercontent.com/tyeth/Adafruit_Wippersnapper_Offline_Configurator/refs/heads/main/wippersnapper_components.json'; //'wippersnapper_components.json';
const BOARDS_JSON_URL = 'https://raw.githubusercontent.com/tyeth/Adafruit_Wippersnapper_Offline_Configurator/refs/heads/use_boards_sd_card/wippersnapper_boards.json'; //'wippersnapper_boards.json';
const COMPONENTS_JSON_URL = 'https://raw.githubusercontent.com/tyeth/Adafruit_Wippersnapper_Offline_Configurator/refs/heads/use_boards_sd_card/wippersnapper_components.json'; //'wippersnapper_components.json';
// Global app state
const appState = {
@ -10,6 +10,7 @@ const appState = {
componentsData: null,
isLoading: false,
loadError: null,
enableautoConfig: false,
// Application state properties (from original code)
selectedBoard: null,
@ -47,6 +48,14 @@ async function loadWippersnapperData() {
await new Promise(resolve => setTimeout(resolve, 1000));
const boardsData = window['jsonBoardObject'];
const componentsData = window['jsonComponentsObject'];
const firmwareData = window['FIRMWARE_DATA'];
if (firmwareData && firmwareData.releaseInfo) {
// Update the release name and link in the UI
document.getElementById('release_name').innerHTML = "(" + firmwareData.releaseInfo.publishedDate + ")<br/>" + firmwareData.releaseInfo.name;
document.getElementById('release_name').href = firmwareData.releaseInfo.url;
//TODO: set onchange for the boards to alter the download url in #firmware_file link
}
appState.boardsData = boardsData.boards;
appState.componentsData = componentsData.components;
document.body.removeChild(componentsObject);
@ -58,6 +67,10 @@ async function loadWippersnapperData() {
appState.componentsData.i2c.push({
id: 'pca9546',
name: 'PCA9546 I2C Multiplexer',
displayName: 'PCA9546 I2C Multiplexer',
productURL: 'https://www.adafruit.com/product/5664',
documentationURL: 'https://learn.adafruit.com/adafruit-pca9546-4-channel-stemma-qt-multiplexer',
image: 'https://cdn-shop.adafruit.com/640x480/5664-00.jpg',
address: '0x70',
addresses: ['0x70', '0x71', '0x72', '0x73', '0x74', '0x75', '0x76', '0x77'],
dataTypes: [],
@ -68,6 +81,10 @@ async function loadWippersnapperData() {
appState.componentsData.i2c.push({
id: 'pca9548',
name: 'PCA9548 I2C Multiplexer',
displayName: 'PCA9548 I2C Multiplexer',
productURL: 'https://www.adafruit.com/product/5626',
documentationURL: 'https://learn.adafruit.com/adafruit-pca9548-8-channel-stemma-qt-qwiic-i2c-multiplexer',
image: 'https://cdn-shop.adafruit.com/640x480/5626-06.jpg',
address: '0x70',
addresses: ['0x70', '0x71', '0x72', '0x73', '0x74', '0x75', '0x76', '0x77'],
dataTypes: [],
@ -78,6 +95,10 @@ async function loadWippersnapperData() {
appState.componentsData.i2c.push({
id: 'tca9546',
name: 'TCA9546 I2C Multiplexer',
displayName: 'TCA9546 4Ch I2C Multiplexer',
productURL: 'https://www.adafruit.com/product/5663',
documentationURL: 'https://learn.adafruit.com/adafruit-pca9546-4-channel-i2c-multiplexer',
image: 'https://cdn-shop.adafruit.com/640x480/5663-04.jpg',
address: '0x70',
addresses: ['0x70', '0x71', '0x72', '0x73', '0x74', '0x75', '0x76', '0x77'],
dataTypes: [],
@ -88,6 +109,10 @@ async function loadWippersnapperData() {
appState.componentsData.i2c.push({
id: 'tca9548',
name: 'TCA9548 I2C Multiplexer',
displayName: 'TCA9548 8Ch I2C Multiplexer',
productURL: 'https://www.adafruit.com/product/2717',
documentationURL: 'https://learn.adafruit.com/adafruit-tca9548a-1-to-8-i2c-multiplexer-breakout',
image: 'https://cdn-shop.adafruit.com/640x480/2717-00.jpg',
address: '0x70',
addresses: ['0x70', '0x71', '0x72', '0x73', '0x74', '0x75', '0x76', '0x77'],
dataTypes: [],
@ -136,16 +161,19 @@ function populateBoardSelect() {
// Filter boards to only include those with UF2 install method
const filteredBoards = Object.entries(appState.boardsData)
.filter(([boardId, board]) => ['uf2', 'web-native-usb'].includes(board.installMethod));
.filter(([boardId, board]) => board.installMethod === 'uf2'); //['uf2', 'web-native-usb'].includes(board.installMethod)); //funhouse
// Sort boards by vendor and name
const sortedBoards = filteredBoards
.sort((a, b) => {
const vendorA = a[1].vendor || '';
const vendorB = b[1].vendor || '';
// Sort by vendor first
if (vendorA !== vendorB) {
if (vendorA === 'Generic') {
return 1; // Sort Generic to the end
} else if (vendorB === 'Generic') {
return -1; // Sort Generic to the end
} else if (vendorA !== vendorB) {
// Sort by vendor first
return vendorA.localeCompare(vendorB);
}
@ -191,19 +219,14 @@ function convertBoardDataToConfig(boardId) {
// Extract pin numbers from board data
const pins = boardData.pins.map(pin => pin.number).filter(num => !isNaN(num));
boardConfig = boardData;
// Create board config
const boardConfig = {
referenceVoltage: boardData.referenceVoltage,
totalGPIOPins: boardData.totalGPIOPins,
totalAnalogPins: boardData.totalAnalogPins || 0,
defaultI2C: {
scl: boardData.defaultI2C.SCL,
sda: boardData.defaultI2C.SDA
},
pins: pins,
displayName: boardData.displayName,
image: boardData.image
};
boardConfig.totalAnalogPins= boardData.totalAnalogPins || 0;
boardConfig.defaultI2C= {
SCL: boardData.defaultI2C.SCL,
SDA: boardData.defaultI2C.SDA
};
boardConfig.pins= pins;
return boardConfig;
}
@ -229,6 +252,7 @@ function convertComponentsDataToConfig() {
componentsConfig.i2c.push({
id: component.id,
name: component.name,
displayName: component.displayName || component.name,
address: component.address || '0x00',
addresses: component.addresses || [component.address || '0x00'],
dataTypes: component.dataTypes || [],
@ -242,6 +266,7 @@ function convertComponentsDataToConfig() {
appState.componentsData.ds18x20.forEach(component => {
componentsConfig.ds18x20.push({
id: component.id,
displayName: component.displayName || component.name,
name: component.name,
dataTypes: component.dataTypes || []
});
@ -253,6 +278,7 @@ function convertComponentsDataToConfig() {
appState.componentsData.pin.forEach(component => {
componentsConfig.pin.push({
id: component.id,
displayName: component.displayName || component.name,
name: component.name,
dataTypes: component.dataTypes || []
});
@ -264,6 +290,7 @@ function convertComponentsDataToConfig() {
appState.componentsData.pixel.forEach(component => {
componentsConfig.pixel.push({
id: component.id,
displayName: component.displayName || component.name,
name: component.name,
dataTypes: component.dataTypes || []
});
@ -275,6 +302,7 @@ function convertComponentsDataToConfig() {
appState.componentsData.pwm.forEach(component => {
componentsConfig.pwm.push({
id: component.id,
displayName: component.displayName || component.name,
name: component.name,
dataTypes: component.dataTypes || []
});
@ -286,6 +314,7 @@ function convertComponentsDataToConfig() {
appState.componentsData.servo.forEach(component => {
componentsConfig.servo.push({
id: component.id,
displayName: component.displayName || component.name,
name: component.name,
dataTypes: component.dataTypes || []
});
@ -297,6 +326,7 @@ function convertComponentsDataToConfig() {
appState.componentsData.uart.forEach(component => {
componentsConfig.uart.push({
id: component.id,
displayName: component.displayName || component.name,
name: component.name,
dataTypes: component.dataTypes || []
});
@ -310,81 +340,21 @@ function convertComponentsDataToConfig() {
* Attach event listeners to the UI elements
*/
function attachEventListeners() {
// Board selection handler
document.getElementById('board-select').addEventListener('change', function() {
const boardId = this.value;
if (!boardId) {
document.getElementById('board-details').classList.add('hidden');
hideSubsequentSections();
return;
}
// Convert board data to config format
const boardConfig = convertBoardDataToConfig(boardId);
appState.selectedBoard = {
id: boardId,
...boardConfig
};
// Update board details display
document.getElementById('ref-voltage').textContent = boardConfig.referenceVoltage;
document.getElementById('total-gpio').textContent = boardConfig.totalGPIOPins;
document.getElementById('total-analog').textContent = boardConfig.totalAnalogPins;
document.getElementById('default-scl').textContent = boardConfig.defaultI2C.scl;
document.getElementById('default-sda').textContent = boardConfig.defaultI2C.sda;
document.getElementById('board-details').classList.remove('hidden');
// If there's a board image, show it
const boardImageElem = document.getElementById('board-image');
if (boardImageElem) {
if (boardConfig.image) {
if (!boardConfig.image.startsWith('http')) {
boardImageElem.src = "https://raw.githubusercontent.com/adafruit/Wippersnapper_Boards/refs/heads/main/" + boardConfig.image;
} else {
boardImageElem.src = boardConfig.image;
}
boardImageElem.classList.remove('hidden');
} else {
boardImageElem.classList.add('hidden');
}
}
// Set up default I2C bus
appState.i2cBuses = [{
id: 'default',
scl: boardConfig.defaultI2C.scl,
sda: boardConfig.defaultI2C.sda
}];
// Update default I2C bus display
document.getElementById('default-i2c-scl').textContent = boardConfig.defaultI2C.scl;
document.getElementById('default-i2c-sda').textContent = boardConfig.defaultI2C.sda;
// Mark default I2C pins as used
appState.usedPins.add(boardConfig.defaultI2C.scl);
appState.usedPins.add(boardConfig.defaultI2C.sda);
// Show companion board section
document.getElementById('companion-board-section').classList.remove('hidden');
// Reset subsequent sections
resetSubsequentSelections();
// Initialize SD and RTC sections based on board
initializeManualConfig();
// Initialize pins lists for SD and I2C configuration
populatePinsLists();
// Convert component data to config format
const componentsConfig = convertComponentsDataToConfig();
// Initialize components sections with the loaded data
populateComponentLists(componentsConfig);
});
// BOARD SELECTION HANDLER HAS BEEN REMOVED
// The duplicate event handler from load-wippersnapper-data.js has been removed
// to prevent conflicts with the handler in wippersnapper-config-builder.js
// Remaining event listeners should be added here or in the original script
// ...
// Instead, we'll prepare the data in the format expected by wippersnapper-config-builder.js
console.log('Data loading complete, board selection handler is in wippersnapper-config-builder.js');
// Convert component data to config format
const componentsConfig = convertComponentsDataToConfig();
console.log('not using Components data converted to config format:', componentsConfig);
// Update the components data in appState with the converted format
// so it's ready for use in the other script
// appState.componentsData = componentsConfig;
// No other event listeners needed here as they are handled in wippersnapper-config-builder.js
}
/**

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
requests

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff