update some deisng parameters and add new cases
This commit is contained in:
@@ -11,6 +11,7 @@ require "yaml"
|
||||
|
||||
require "highline"
|
||||
|
||||
require_relative "feather_case_wizard/prompting_ui"
|
||||
require_relative "feather_case_wizard/config"
|
||||
require_relative "feather_case_wizard/toolchain"
|
||||
require_relative "feather_case_wizard/package_builder"
|
||||
@@ -23,4 +24,3 @@ module FeatherCaseWizard
|
||||
MODEL_SOURCE = ROOT.join("feather_case.scad").freeze
|
||||
RENDERER_SOURCE = ROOT.join("tools/render_stl_previews.py").freeze
|
||||
end
|
||||
|
||||
|
||||
@@ -5,18 +5,18 @@ module FeatherCaseWizard
|
||||
def initialize(ui: HighLine.new, cases_root: CASES_ROOT,
|
||||
model_source: MODEL_SOURCE, renderer_source: RENDERER_SOURCE,
|
||||
toolchain: Toolchain.new)
|
||||
@ui = ui
|
||||
@ui = PromptingUI.wrap(ui)
|
||||
@cases_root = Pathname(cases_root)
|
||||
@model_source = Pathname(model_source)
|
||||
@renderer_source = Pathname(renderer_source)
|
||||
@toolchain = toolchain
|
||||
@wizard = Wizard.new(ui: ui)
|
||||
@wizard = Wizard.new(ui: @ui)
|
||||
end
|
||||
|
||||
def run
|
||||
@ui.say(@ui.color("Feather Case Wizard", :bold))
|
||||
loop do
|
||||
action = @ui.choose("\nWhat would you like to do?") do |menu|
|
||||
action = @ui.choose("What would you like to do?") do |menu|
|
||||
menu.choice("Create a new case") { :create }
|
||||
menu.choice("Edit an existing case") { :edit }
|
||||
menu.choice("Rebuild an existing case") { :rebuild }
|
||||
|
||||
@@ -14,13 +14,14 @@ module FeatherCaseWizard
|
||||
DEFAULT_PARAMETERS = {
|
||||
"top_profile" => "generic",
|
||||
"wing_count" => 1,
|
||||
"wing_pitch" => 11.0,
|
||||
"wing_pitch" => 12.25,
|
||||
"stack_height_override" => 0.0,
|
||||
"generic_component_height" => 3.0,
|
||||
"generic_top_gap" => 2.0,
|
||||
"gps_antenna_height" => 4.0,
|
||||
"gps_antenna_height" => 6.0,
|
||||
"gps_top_gap" => 5.0,
|
||||
"oled_component_height" => 2.4,
|
||||
"oled_display_height" => 1.5,
|
||||
"oled_face_gap" => 0.8,
|
||||
"button_a" => true,
|
||||
"button_b" => true,
|
||||
@@ -62,13 +63,19 @@ module FeatherCaseWizard
|
||||
"board_bottom_clearance" => 5.4,
|
||||
"board_side_clearance" => 7.0,
|
||||
"board_rear_clearance" => 4.0,
|
||||
"usb_board_wall_gap" => 1.2,
|
||||
"usb_board_wall_gap" => 2.0,
|
||||
"lid_lip_depth" => 2.4,
|
||||
"lid_lip_thickness" => 1.2,
|
||||
"fit_clearance" => 0.35,
|
||||
"cutout_min_web" => 1.2,
|
||||
"hole_compensation" => 0.2,
|
||||
"lid_screw_head_diameter" => 5.2,
|
||||
"lid_screw_head_height" => 2.5,
|
||||
"usb_opening_enabled" => true,
|
||||
"usb_guide_enabled" => true,
|
||||
"usb_connector_overhang" => 2.0,
|
||||
"usb_guide_wall_thickness" => 1.2,
|
||||
"usb_guide_end_gap" => 1.0,
|
||||
"usb_opening_width" => 12.5,
|
||||
"usb_opening_height" => 7.0,
|
||||
"usb_opening_radius" => 1.5,
|
||||
@@ -77,11 +84,12 @@ module FeatherCaseWizard
|
||||
|
||||
ADVANCED_FIELDS = {
|
||||
"Stack" => %w[wing_pitch generic_component_height generic_top_gap
|
||||
gps_antenna_height gps_top_gap oled_component_height oled_face_gap],
|
||||
gps_antenna_height gps_top_gap oled_component_height
|
||||
oled_display_height oled_face_gap],
|
||||
"Shell and fit" => %w[wall_thickness floor_thickness lid_thickness corner_radius
|
||||
board_bottom_clearance board_side_clearance board_rear_clearance
|
||||
usb_board_wall_gap lid_lip_depth lid_lip_thickness fit_clearance
|
||||
hole_compensation lid_screw_head_diameter lid_screw_head_height],
|
||||
board_bottom_clearance board_side_clearance lid_lip_depth
|
||||
lid_lip_thickness fit_clearance cutout_min_web hole_compensation
|
||||
lid_screw_head_diameter lid_screw_head_height],
|
||||
"M2.5 inserts" => %w[m25_insert_length m25_insert_hole_diameter
|
||||
m25_screw_clearance m25_boss_diameter
|
||||
insert_entry_chamfer insert_melt_relief],
|
||||
@@ -93,7 +101,8 @@ module FeatherCaseWizard
|
||||
switch_flange_diameter switch_body_length
|
||||
switch_body_diameter switch_wire_allowance],
|
||||
"USB-C opening" => %w[usb_opening_width usb_opening_height
|
||||
usb_opening_radius usb_vertical_offset],
|
||||
usb_opening_radius usb_vertical_offset usb_connector_overhang
|
||||
usb_guide_wall_thickness usb_guide_end_gap],
|
||||
"OLED buttons" => %w[button_projection button_flange_thickness
|
||||
button_travel button_contact_extension]
|
||||
}.freeze
|
||||
@@ -264,6 +273,9 @@ module FeatherCaseWizard
|
||||
unless (outputs["coupons"] - COUPONS).empty?
|
||||
raise ConfigError, "Unsupported calibration coupon"
|
||||
end
|
||||
if parameters["usb_guide_enabled"] && !parameters["usb_opening_enabled"]
|
||||
raise ConfigError, "USB guide requires the USB opening"
|
||||
end
|
||||
|
||||
positive = DEFAULT_PARAMETERS.keys - %w[usb_vertical_offset stack_height_override]
|
||||
positive.each do |key|
|
||||
@@ -289,7 +301,10 @@ module FeatherCaseWizard
|
||||
def normalize!
|
||||
data["schema_version"] = Integer(data.fetch("schema_version", SCHEMA_VERSION))
|
||||
data["generator"] ||= {}
|
||||
data["parameters"] = self.class.deep_copy(DEFAULT_PARAMETERS).merge(data["parameters"] || {})
|
||||
supplied_parameters = data["parameters"] || {}
|
||||
legacy_usb_guide = !supplied_parameters.key?("usb_guide_enabled")
|
||||
data["parameters"] = self.class.deep_copy(DEFAULT_PARAMETERS).merge(supplied_parameters)
|
||||
data["parameters"]["usb_guide_enabled"] = false if legacy_usb_guide
|
||||
data["cutouts"] ||= []
|
||||
data["outputs"] ||= {}
|
||||
data["outputs"]["formats"] ||= ["3mf"]
|
||||
@@ -302,6 +317,7 @@ module FeatherCaseWizard
|
||||
def validate_cutouts!
|
||||
dims = dimensions
|
||||
names = {}
|
||||
bounds = []
|
||||
cutouts.each do |cutout|
|
||||
name = cutout["name"].to_s.strip
|
||||
raise ConfigError, "Every cutout needs a name" if name.empty?
|
||||
@@ -328,9 +344,68 @@ module FeatherCaseWizard
|
||||
radius = Float(cutout.fetch("radius", [width, height].min / 2))
|
||||
raise ConfigError, "#{name} dimensions must be positive" unless
|
||||
width.positive? && height.positive? && radius.positive?
|
||||
bounds << {
|
||||
"name" => name, "face" => face,
|
||||
"left" => horizontal - width / 2, "right" => horizontal + width / 2,
|
||||
"bottom" => z - height / 2, "top" => z + height / 2
|
||||
}
|
||||
rescue ArgumentError, TypeError, KeyError => e
|
||||
raise ConfigError, "Invalid cutout #{name.inspect}: #{e.message}"
|
||||
end
|
||||
validate_cutout_spacing!(bounds, dims)
|
||||
end
|
||||
|
||||
def validate_cutout_spacing!(bounds, dims)
|
||||
web = parameters["cutout_min_web"]
|
||||
bounds.combination(2) do |first, second|
|
||||
next unless first["face"] == second["face"]
|
||||
next if bounds_separated?(first, second, web)
|
||||
|
||||
raise ConfigError,
|
||||
"#{first["name"]} and #{second["name"]} are too close on the #{first["face"]} wall"
|
||||
end
|
||||
return unless parameters["usb_opening_enabled"]
|
||||
|
||||
usb = usb_wall_bounds(dims)
|
||||
bounds.select { |item| item["face"] == "front" }.each do |cutout|
|
||||
next if bounds_separated?(cutout, usb, web)
|
||||
|
||||
raise ConfigError, "#{cutout["name"]} is too close to the USB opening or guide"
|
||||
end
|
||||
end
|
||||
|
||||
def bounds_separated?(first, second, web)
|
||||
first["right"] + web <= second["left"] ||
|
||||
second["right"] + web <= first["left"] ||
|
||||
first["top"] + web <= second["bottom"] ||
|
||||
second["top"] + web <= first["bottom"]
|
||||
end
|
||||
|
||||
def usb_wall_bounds(dims)
|
||||
p = parameters
|
||||
effective_floor = p["tripod_insert_enabled"] ?
|
||||
[p["floor_thickness"], p["tripod_insert_length"] + p["insert_melt_relief"] + 1.2].max :
|
||||
p["floor_thickness"]
|
||||
effective_board_clearance = [
|
||||
p["board_bottom_clearance"],
|
||||
p["m25_insert_length"] + p["insert_melt_relief"] + 0.6
|
||||
].max
|
||||
board_top_z = effective_floor + effective_board_clearance + 1.6
|
||||
global_y = p["wall_thickness"] + p["board_side_clearance"] + 11.43
|
||||
horizontal = dims["width"] - global_y
|
||||
z = board_top_z + 1.6 + p["usb_vertical_offset"]
|
||||
guide_length = [
|
||||
p["usb_board_wall_gap"] - p["usb_connector_overhang"] - p["usb_guide_end_gap"], 0
|
||||
].max
|
||||
guide_active = p["usb_guide_enabled"] && guide_length.positive?
|
||||
shell = guide_active ? 2 * p["usb_guide_wall_thickness"] : 0
|
||||
width = p["usb_opening_width"] + p["hole_compensation"] + shell
|
||||
height = p["usb_opening_height"] + p["hole_compensation"] + shell
|
||||
{
|
||||
"name" => "USB opening", "face" => "front",
|
||||
"left" => horizontal - width / 2, "right" => horizontal + width / 2,
|
||||
"bottom" => z - height / 2, "top" => z + height / 2
|
||||
}
|
||||
end
|
||||
|
||||
def stringify_keys(value)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module FeatherCaseWizard
|
||||
class PromptingUI
|
||||
def self.wrap(ui)
|
||||
ui.is_a?(self) ? ui : new(ui)
|
||||
end
|
||||
|
||||
def initialize(ui)
|
||||
@ui = ui
|
||||
end
|
||||
|
||||
def ask(*arguments, &block)
|
||||
separate_after { @ui.ask(*arguments, &block) }
|
||||
end
|
||||
|
||||
def agree(*arguments, &block)
|
||||
separate_after { @ui.agree(*arguments, &block) }
|
||||
end
|
||||
|
||||
def choose(prompt, &block)
|
||||
separate_after do
|
||||
@ui.choose do |menu|
|
||||
menu.header = prompt.to_s.sub(/[?:]\s*\z/, "")
|
||||
block.call(menu)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def say(*arguments, &block)
|
||||
@ui.say(*arguments, &block)
|
||||
end
|
||||
|
||||
def color(*arguments, &block)
|
||||
@ui.color(*arguments, &block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def separate_after
|
||||
result = yield
|
||||
@ui.say("\n")
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,7 @@ module FeatherCaseWizard
|
||||
attr_reader :ui
|
||||
|
||||
def initialize(ui: HighLine.new)
|
||||
@ui = ui
|
||||
@ui = PromptingUI.wrap(ui)
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -15,14 +15,14 @@ module FeatherCaseWizard
|
||||
end
|
||||
|
||||
def edit(config)
|
||||
ui.say("\nEditing #{config.name} (`#{config.slug}`)")
|
||||
ui.say("Editing #{config.name} (`#{config.slug}`)")
|
||||
config.data["name"] = text("Case name", config.name)
|
||||
configure(config, new_case: false)
|
||||
end
|
||||
|
||||
def confirm_build(config)
|
||||
dims = config.dimensions
|
||||
ui.say("\n#{ui.color("Build summary", :bold)}")
|
||||
ui.say(ui.color("Build summary", :bold))
|
||||
ui.say(" Case: #{config.name} (#{config.slug})")
|
||||
ui.say(" Profile: #{config.parameters["top_profile"]}")
|
||||
ui.say(" Wings: #{config.parameters["wing_count"]}")
|
||||
@@ -42,6 +42,7 @@ module FeatherCaseWizard
|
||||
choose_formats(config)
|
||||
choose_profile(config)
|
||||
choose_stack(config)
|
||||
choose_end_clearance(config)
|
||||
choose_features(config)
|
||||
manage_cutouts(config)
|
||||
choose_coupons(config)
|
||||
@@ -63,7 +64,7 @@ module FeatherCaseWizard
|
||||
menu.choice("3MF") { ["3mf"] }
|
||||
menu.choice("STL") { ["stl"] }
|
||||
menu.choice("Both 3MF and STL") { %w[3mf stl] }
|
||||
menu.default = label == "both" ? 3 : (label == "stl" ? 2 : 1)
|
||||
menu.default = (label == "both" ? 3 : (label == "stl" ? 2 : 1)).to_s
|
||||
end
|
||||
config.outputs["formats"] = selected
|
||||
end
|
||||
@@ -74,7 +75,7 @@ module FeatherCaseWizard
|
||||
menu.choice("Generic / other") { "generic" }
|
||||
menu.choice("GPS") { "gps" }
|
||||
menu.choice("128×64 OLED") { "oled128x64" }
|
||||
menu.default = {"generic" => 1, "gps" => 2, "oled128x64" => 3}.fetch(current)
|
||||
menu.default = {"generic" => 1, "gps" => 2, "oled128x64" => 3}.fetch(current).to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,11 +83,11 @@ module FeatherCaseWizard
|
||||
p = config.parameters
|
||||
p["wing_count"] = integer("Number of FeatherWings", p["wing_count"], 1..8)
|
||||
measured = p["stack_height_override"].positive?
|
||||
if ui.agree("Use a measured board-to-top-PCB stack height? (y/n) ") do |question|
|
||||
if ui.agree("Use a measured Feather-PCB-top to top-Wing-PCB stack height? (y/n) ") do |question|
|
||||
question.default = measured ? "yes" : "no"
|
||||
end
|
||||
p["stack_height_override"] = number(
|
||||
"Measured height in mm", measured ? p["stack_height_override"] :
|
||||
"PCB-top to PCB-top height in mm", measured ? p["stack_height_override"] :
|
||||
p["wing_count"] * p["wing_pitch"], min: 0.1
|
||||
)
|
||||
else
|
||||
@@ -96,22 +97,55 @@ module FeatherCaseWizard
|
||||
|
||||
def choose_features(config)
|
||||
p = config.parameters
|
||||
p["usb_opening_enabled"] = agreement(
|
||||
"Include front USB-C programming opening?", p["usb_opening_enabled"]
|
||||
)
|
||||
p["usb_guide_enabled"] = if p["usb_opening_enabled"]
|
||||
agreement("Include inward USB plug guide?",
|
||||
p["usb_guide_enabled"])
|
||||
else
|
||||
false
|
||||
end
|
||||
p["battery_enabled"] = agreement("Include 18650 battery compartment and switch?",
|
||||
p["battery_enabled"])
|
||||
p["tripod_insert_enabled"] = agreement("Include centered 1/4-20 insert?",
|
||||
p["tripod_insert_enabled"])
|
||||
return unless p["top_profile"] == "oled128x64"
|
||||
|
||||
ui.say("\nOLED captive buttons")
|
||||
ui.say("OLED captive buttons")
|
||||
p["button_a"] = agreement("Enable button A?", p["button_a"])
|
||||
p["button_b"] = agreement("Enable button B?", p["button_b"])
|
||||
p["button_c"] = agreement("Enable button C?", p["button_c"])
|
||||
p["button_reset"] = agreement("Enable reset button?", p["button_reset"])
|
||||
end
|
||||
|
||||
def choose_end_clearance(config)
|
||||
p = config.parameters
|
||||
defaults = Config::DEFAULT_PARAMETERS
|
||||
custom = p["usb_board_wall_gap"] != defaults["usb_board_wall_gap"] ||
|
||||
p["board_rear_clearance"] != defaults["board_rear_clearance"]
|
||||
enabled = ui.agree("Use extra clearance beyond the Feather PCB ends? (y/n) ") do |question|
|
||||
question.default = custom ? "yes" : "no"
|
||||
end
|
||||
unless enabled
|
||||
p["usb_board_wall_gap"] = defaults["usb_board_wall_gap"]
|
||||
p["board_rear_clearance"] = defaults["board_rear_clearance"]
|
||||
return
|
||||
end
|
||||
|
||||
p["usb_board_wall_gap"] = number(
|
||||
"USB/front PCB-edge to inner-wall clearance (mm)",
|
||||
p["usb_board_wall_gap"], min: 0.1
|
||||
)
|
||||
p["board_rear_clearance"] = number(
|
||||
"Rear PCB-edge to inner-wall clearance (mm)",
|
||||
p["board_rear_clearance"], min: 0.1
|
||||
)
|
||||
end
|
||||
|
||||
def manage_cutouts(config)
|
||||
loop do
|
||||
ui.say("\nCustom cable cutouts: #{config.cutouts.empty? ? "none" :
|
||||
ui.say("Custom cable cutouts: #{config.cutouts.empty? ? "none" :
|
||||
config.cutouts.map { |cutout| cutout["name"] }.join(", ")}")
|
||||
action = ui.choose("Cable cutouts") do |menu|
|
||||
menu.choice("Done") { :done }
|
||||
@@ -141,12 +175,15 @@ module FeatherCaseWizard
|
||||
dims = config.dimensions
|
||||
name = text("Cutout name", existing["name"] || "cable")
|
||||
face = ui.choose("Wall") do |menu|
|
||||
Config::FACES.each { |item| menu.choice(item) { item } }
|
||||
menu.default = Config::FACES.index(existing["face"]).to_i + 1 if existing["face"]
|
||||
menu.choice("Front — USB-C short wall") { "front" }
|
||||
menu.choice("Rear — opposite short wall") { "rear" }
|
||||
menu.choice("Left — plain/non-JST long wall") { "left" }
|
||||
menu.choice("Right — JST/battery long wall") { "right" }
|
||||
menu.default = (Config::FACES.index(existing["face"]).to_i + 1).to_s if existing["face"]
|
||||
end
|
||||
shape = ui.choose("Shape") do |menu|
|
||||
Config::SHAPES.each { |item| menu.choice(item) { item } }
|
||||
menu.default = Config::SHAPES.index(existing["shape"]).to_i + 1 if existing["shape"]
|
||||
menu.default = (Config::SHAPES.index(existing["shape"]).to_i + 1).to_s if existing["shape"]
|
||||
end
|
||||
wall_width = %w[front rear].include?(face) ? dims["width"] : dims["length"]
|
||||
ui.say("Viewing the #{face} wall from outside: width #{format("%.1f", wall_width)} mm; " \
|
||||
@@ -209,7 +246,7 @@ module FeatherCaseWizard
|
||||
Config::ADVANCED_FIELDS.each do |section, fields|
|
||||
next unless ui.agree("Adjust #{section.downcase}? (y/n) ") { |q| q.default = "no" }
|
||||
|
||||
ui.say("\n#{section}")
|
||||
ui.say(section)
|
||||
fields.each do |field|
|
||||
config.parameters[field] = number(field.tr("_", " "), config.parameters[field],
|
||||
min: field == "usb_vertical_offset" ? nil : 0.01)
|
||||
@@ -286,4 +323,3 @@ module FeatherCaseWizard
|
||||
def yes_no(value) = value ? "yes" : "no"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user