firestarter 3.2.5__tar.gz → 3.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {firestarter-3.2.5/firestarter.egg-info → firestarter-3.2.6}/PKG-INFO +1 -1
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/_version.py +3 -3
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/cli_handlers.py +4 -2
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/codec.py +3 -2
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/firmware.py +8 -6
- firestarter-3.2.6/firestarter/flash4_erase_gate.py +104 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/ic_layout.py +7 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/serial_comm.py +10 -1
- {firestarter-3.2.5 → firestarter-3.2.6/firestarter.egg-info}/PKG-INFO +1 -1
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/scm_version.json +2 -2
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_codec.py +2 -2
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_codec_format_message.py +2 -2
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_decoder.py +3 -3
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_firmware_install.py +32 -28
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_flash4_erase_gate.py +98 -102
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_info_vpp_and_erase.py +3 -2
- firestarter-3.2.5/firestarter/flash4_erase_gate.py +0 -79
- {firestarter-3.2.5 → firestarter-3.2.6}/LICENSE +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/MANIFEST.in +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/README.md +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/__init__.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/address_parser.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/avr_tool.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/channel.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/chip_resolver.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/chip_test.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/compare.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/config.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/constants.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/data/chip_database.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/data/pinouts.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/database.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/diagnostic_report.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/dip24_2816_erase_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/eprom_info.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/eprom_operations.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/erase_support.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/exceptions.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/frame_parser.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/fw_release_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/hardware.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/jp5_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/jumper_table.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/lock_status.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/log_capture.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/logging_utils.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/main.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/messages.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/page_size_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/protection_readability.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/py32_dfu.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/sdp_capability.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/sdp_honesty.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/submit.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/timestamps.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/transport_counters.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/utils.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/vpp_display.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/write_blank_guard.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/SOURCES.txt +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/dependency_links.txt +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/entry_points.txt +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/requires.txt +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/scm_file_list.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/top_level.txt +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/pyproject.toml +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/setup.cfg +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/__init__.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/__snapshots__/test_characterization.ambr +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/conftest.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fake_chip.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/part_number_delta.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/report_shapes.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/at28c256-full-all-ok-sdp.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/attr01-status-axis-transport-fault.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh20-at28c256-fail.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh23-w27e257-fail.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh28-m27c512-fail.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh47-sst27sf512-pass.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-all-ok.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-blank-check-bad.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-canonical-name.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-comma-joined-name.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-runs-1.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/prune03-synthesized-fingerprint-match.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/sst27sf512-full-all-ok.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/sst27sf512-six-step-readback-gated.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/sst27sf512-six-step.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/synthetic-arm4-empty-results.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/synthetic-arm4-no-ok.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/uv-slot-write-pass.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/w27e257-full-all-ok.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/shape_ids.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/synthetic_nonzero_chip_id.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/golden/build_db_part_specific_constants.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/golden/chip_database_field_inventory.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/golden/wire_dict.json +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/plan_corpus.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_address_parser.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_b15_page_size_corroboration.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_blast_radius_invariance.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_boot_block_hint.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_budget_failure_render.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_bug_characterization.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_constant_census.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_inclusion.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_interpret_timing.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_pinout_fork.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_canonical_part_number.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_characterization.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_database_field_inventory.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_name_resolution.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_resolver.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_blank_check_order.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_cycle.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_sdp_leg.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_timing.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_uv_slot_write.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_cli_handlers.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_click_group_gate_hook.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_cobs.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_compare.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_config.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_config_band_refusal.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_connect_cost_harness.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_consistency_check.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_coverage_floor_v18.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_database_conversion.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_datasheet_overrides.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_derive_plan_no_drop_sweep.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_derive_plan_structural_sentinel.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dev_group_channel_gating.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dev_test_cmd.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dev_tools_channel_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_devtest_firmware_error_propagation.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dfu_opcode_anchors.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_diagnostic_report.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dip24_2816_erase_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_endpoint_constants.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_eprom_database.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_eprom_info.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_eprom_operations.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_erase_blank_step_nonregression.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_erase_flag_invariants.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_error_code_seam.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_even_block.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_extra_chips_supplement.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_list_failure_vs_empty.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_port_targeting_and_blind_install.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_release_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_update_dead_endpoint.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_update_path_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_version_guard.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fwguard.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_hardware.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_hw_revision_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_ic_layout.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_jp5_gate.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_jumper_table.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_lock_status_class_partition.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_lock_status_cli.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_lock_status_wire.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_log_capture.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_logging_utils.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_matrix_artifact.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_page_size_alignment_refusal.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_page_size_invariants.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_page_size_write_refusal.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_parse_devtest_issue.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_probe_spurious_setup_ack.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_programming_vcc_census.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protect_flags_doc_measurements.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protection_resolution.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protection_status_catalog.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protection_table_citations.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protocol_not_implemented.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protocol_not_implemented_production_path.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_provenance.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_pulse_us_override.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_channel_gating.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_dfu.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_flash_map_host.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_pyusb_absent.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_pyusb_api_surface.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_sdp_capability.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_sdp_db_invariant.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_sdp_honesty.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_serial_characterization.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_serial_comm.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_session_lease.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_setup_refusals.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_submit.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_timestamps.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_transport_counters.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_utils.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_uv_mask.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_validate_family_cmd.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_validate_oracle.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_validated_parts_regression_surface.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_vcc_margin_rail.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_vpp_decode_table.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_vpp_rail_classification.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_wire_dict_equivalence.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_blank_guard.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_blank_guard_pinning.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_guard_port_pinning.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_progress.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_response_budget.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_skip_erase_0x0d.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_skip_sdp_unlock.py +0 -0
- {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firestarter
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.6
|
|
4
4
|
Summary: EPROM programmer software for the Relatively Universal ROM Programmer
|
|
5
5
|
Author-email: Henrik Olsson <henols@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/henols/firestarter_app
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '3.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 2,
|
|
21
|
+
__version__ = version = '3.2.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 2, 6)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g6a02a12'
|
|
@@ -1443,9 +1443,11 @@ def erase(
|
|
|
1443
1443
|
|
|
1444
1444
|
eprom_data = resolve_chip(eprom, db=app.db)
|
|
1445
1445
|
|
|
1446
|
+
# The database leaves FLAG_CAN_ERASE clear for flash4, so `write` never
|
|
1447
|
+
# carries it. The erase sets it for this command only; setup_command
|
|
1448
|
+
# refuses it on firmware without the 5 V software chip erase.
|
|
1446
1449
|
if flash4_erase_gate.is_flash4(eprom_data):
|
|
1447
|
-
|
|
1448
|
-
sys.exit(0 if ignore_unsupported else 1)
|
|
1450
|
+
eprom_data = flash4_erase_gate.with_erase_flag(eprom_data)
|
|
1449
1451
|
|
|
1450
1452
|
full_record = app.db.get_eprom(eprom) or {}
|
|
1451
1453
|
if dip24_2816_erase_gate.is_affected(full_record.get("pin-map")):
|
|
@@ -74,7 +74,8 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
|
|
|
74
74
|
|
|
75
75
|
P-02 MSG_OK_REV — params[0]=physical u8, params[1]=effective u8
|
|
76
76
|
effective==0xFF → "Rev{physical}" (no override)
|
|
77
|
-
effective!=0xFF → "Rev{effective},
|
|
77
|
+
effective!=0xFF → "Rev{effective}, Detected HW: Rev{physical}"
|
|
78
|
+
(the override is in effect; the second field is what the board detected)
|
|
78
79
|
|
|
79
80
|
P-03 MSG_OK_CFG — params[0]=r1 u32, params[1]=r2 u32, params[2]=override u8,
|
|
80
81
|
params[3]=bandgap_mv u16 (absent on firmware before 3.1.0b4)
|
|
@@ -104,7 +105,7 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
|
|
|
104
105
|
if effective == 0xFF:
|
|
105
106
|
return phys_str
|
|
106
107
|
eff_str = _REVISION_SILKSCREEN.get(effective, f"Rev{effective}")
|
|
107
|
-
return f"{eff_str},
|
|
108
|
+
return f"{eff_str}, Detected HW: {phys_str}"
|
|
108
109
|
|
|
109
110
|
if msg_id == MSG_OK_CFG and len(params) in (3, 4):
|
|
110
111
|
r1, r2, override = params[0], params[1], params[2]
|
|
@@ -335,8 +335,8 @@ class FirmwareManager:
|
|
|
335
335
|
"""Router: returns (resolved_version, download_url) or (None, None) on failure.
|
|
336
336
|
|
|
337
337
|
channel='stable' → delegates to fetch_latest_release_info (back-compat shim).
|
|
338
|
-
channel='pre' → paginates /releases,
|
|
339
|
-
|
|
338
|
+
channel='pre' → paginates /releases, takes the highest non-draft release by
|
|
339
|
+
PEP 440, stable or pre-release; falls back to stable if none.
|
|
340
340
|
channel='pinned' → fetches /releases/tags/{version} directly.
|
|
341
341
|
""" # noqa: E501
|
|
342
342
|
if channel == "stable":
|
|
@@ -374,10 +374,12 @@ class FirmwareManager:
|
|
|
374
374
|
logger.error(f"Failed to fetch releases for pre-release channel: {e}")
|
|
375
375
|
return None, None
|
|
376
376
|
|
|
377
|
-
#
|
|
377
|
+
# Every non-draft release, stable or pre-release, as `pip --pre`
|
|
378
|
+
# does: a stable 3.2.2 must win over an older 3.1.1b2. Skip
|
|
379
|
+
# unparseable tags.
|
|
378
380
|
candidates = []
|
|
379
381
|
for r in all_releases:
|
|
380
|
-
if
|
|
382
|
+
if r.get("draft"):
|
|
381
383
|
continue
|
|
382
384
|
tag = r.get("tag_name", "")
|
|
383
385
|
try:
|
|
@@ -387,7 +389,7 @@ class FirmwareManager:
|
|
|
387
389
|
|
|
388
390
|
if not candidates:
|
|
389
391
|
logger.info(
|
|
390
|
-
"No
|
|
392
|
+
"No firmware release with a parseable tag — falling back to stable "
|
|
391
393
|
"(matches pip --pre semantics)."
|
|
392
394
|
)
|
|
393
395
|
return self.fetch_latest_release_info(board=board)
|
|
@@ -787,7 +789,7 @@ class FirmwareManager:
|
|
|
787
789
|
Returns True if an operation (check or install) was successful in some sense, False on major failure.
|
|
788
790
|
|
|
789
791
|
channel='stable' → uses /releases/latest (default, INST-01 non-regression).
|
|
790
|
-
channel='pre' → selects highest pre-release.
|
|
792
|
+
channel='pre' → selects the highest release, stable or pre-release.
|
|
791
793
|
channel='pinned' → uses exact tag from pinned_version.
|
|
792
794
|
""" # noqa: E501
|
|
793
795
|
connected_port, current_version, current_board = self.check_current_firmware(
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Name: Firestarter
|
|
3
|
+
Copyright (c) 2024 Henrik Olsson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted under MIT license.
|
|
6
|
+
|
|
7
|
+
Flash4 (protocol 0x05) erase policy.
|
|
8
|
+
|
|
9
|
+
Firmware 3.2.2 added a software chip erase for protocol 0x05 (the six-cycle
|
|
10
|
+
AA/55/80/AA/55/10 sequence, 5 V only). Older firmware is not safe to send
|
|
11
|
+
that erase to:
|
|
12
|
+
|
|
13
|
+
- 3.0.0b8 to 3.0.0b26 erase a 0x05 part with the VPP regulator and VPE
|
|
14
|
+
switched on: 12 V on a 5 V part.
|
|
15
|
+
- 3.0.0b27 to 3.2.1 answer "not supported".
|
|
16
|
+
|
|
17
|
+
So `FLAG_CAN_ERASE` stays clear for algorithm 5 in the database
|
|
18
|
+
(`erase_support.erase_accepted`), and `write` never carries it. Only the
|
|
19
|
+
`erase` command sets it, for that one command
|
|
20
|
+
(`with_erase_flag`), and `SerialCommunicator.setup_command` refuses the
|
|
21
|
+
command before the operation starts when the firmware is older than
|
|
22
|
+
`ERASE_MIN_FIRMWARE` (`needs_newer_firmware`).
|
|
23
|
+
|
|
24
|
+
Like `jp5_gate.py` and `sdp_capability.py`, this is a pure predicate module:
|
|
25
|
+
no I/O, no environment reads, no serial access.
|
|
26
|
+
|
|
27
|
+
`is_flash4` FAILS OPEN on absent evidence (a wire dict with no `algorithm`
|
|
28
|
+
key is "not flash4"), unlike `jp5_gate` and `sdp_capability`. Refusing every
|
|
29
|
+
chip this predicate cannot classify would break `erase` for chips that are
|
|
30
|
+
not flash4 at all.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import re
|
|
36
|
+
from typing import Any, Mapping # noqa: UP035
|
|
37
|
+
|
|
38
|
+
from firestarter.constants import COMMAND_ERASE, FLAG_CAN_ERASE
|
|
39
|
+
|
|
40
|
+
FLASH4_PROTOCOL_ID = 5
|
|
41
|
+
|
|
42
|
+
# The first firmware with the software chip erase for protocol 0x05.
|
|
43
|
+
ERASE_MIN_FIRMWARE = (3, 2, 2)
|
|
44
|
+
|
|
45
|
+
_FIRMWARE_REFUSAL_FORMAT = (
|
|
46
|
+
"Erasing this chip needs firmware {minimum} or later; this board runs "
|
|
47
|
+
"{version}. Update it with 'firestarter fw --install'."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_flash4(programmer_data: Mapping[str, Any] | None) -> bool:
|
|
52
|
+
"""True when the wire dict's `algorithm` value is the flash4 protocol id.
|
|
53
|
+
|
|
54
|
+
The comparison is against `algorithm`, not `flags & FLAG_CAN_ERASE`: the
|
|
55
|
+
flag is also clear for UV-EPROM and SRAM parts, so a flag-based
|
|
56
|
+
predicate would widen this past its flash4-only scope.
|
|
57
|
+
|
|
58
|
+
Returns False for a missing, `None`, or empty `programmer_data`, and for
|
|
59
|
+
a dict with no `algorithm` key.
|
|
60
|
+
"""
|
|
61
|
+
if not programmer_data:
|
|
62
|
+
return False
|
|
63
|
+
return programmer_data.get("algorithm") == FLASH4_PROTOCOL_ID
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def with_erase_flag(programmer_data: Mapping[str, Any]) -> dict[str, Any]:
|
|
67
|
+
"""A copy of `programmer_data` with `FLAG_CAN_ERASE` set, for `erase` only."""
|
|
68
|
+
data = dict(programmer_data)
|
|
69
|
+
data["flags"] = int(data.get("flags") or 0) | FLAG_CAN_ERASE
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _version_tuple(version: str | None) -> tuple[int, int, int] | None:
|
|
74
|
+
match = re.match(r"(\d+)\.(\d+)\.(\d+)", version or "")
|
|
75
|
+
if not match:
|
|
76
|
+
return None
|
|
77
|
+
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def needs_newer_firmware(
|
|
81
|
+
command: Mapping[str, Any], firmware_version: str | None
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""True when `command` is a flash4 erase that `firmware_version` cannot run safely.
|
|
84
|
+
|
|
85
|
+
Only an erase command that carries `FLAG_CAN_ERASE` for a flash4 part is
|
|
86
|
+
affected; every other command returns False. A version that cannot be
|
|
87
|
+
parsed counts as too old, because the 3.0.0 pre-releases with the 12 V
|
|
88
|
+
erase are older than every version this check accepts. A pre-release
|
|
89
|
+
or development build of 3.2.2 (`3.2.2rc1`, `3.2.2.dev5`) counts as 3.2.2.
|
|
90
|
+
"""
|
|
91
|
+
if command.get("cmd") != COMMAND_ERASE or not is_flash4(command):
|
|
92
|
+
return False
|
|
93
|
+
if not int(command.get("flags") or 0) & FLAG_CAN_ERASE:
|
|
94
|
+
return False
|
|
95
|
+
parsed = _version_tuple(firmware_version)
|
|
96
|
+
return parsed is None or parsed < ERASE_MIN_FIRMWARE
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def firmware_refusal_text(firmware_version: str | None) -> str:
|
|
100
|
+
"""The one-line refusal for a flash4 erase on firmware that is too old."""
|
|
101
|
+
return _FIRMWARE_REFUSAL_FORMAT.format(
|
|
102
|
+
minimum=".".join(str(n) for n in ERASE_MIN_FIRMWARE),
|
|
103
|
+
version=firmware_version or "an unknown version",
|
|
104
|
+
)
|
|
@@ -12,6 +12,7 @@ from typing import Dict, List # noqa: UP035
|
|
|
12
12
|
from firestarter import jumper_table
|
|
13
13
|
from firestarter.database import EpromDatabase, format_mv
|
|
14
14
|
from firestarter.erase_support import erase_accepted, is_electrically_erasable
|
|
15
|
+
from firestarter.flash4_erase_gate import FLASH4_PROTOCOL_ID
|
|
15
16
|
from firestarter.vpp_display import shows_programming_vpp
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger("EpromSpecBuilder")
|
|
@@ -19,6 +20,7 @@ logger = logging.getLogger("EpromSpecBuilder")
|
|
|
19
20
|
CAN_ERASE_YES = "yes (electrically erasable)"
|
|
20
21
|
CAN_ERASE_UV_ONLY = "no (UV erase only)"
|
|
21
22
|
CAN_ERASE_NOT_SUPPORTED = "no (erase is not supported for this chip)"
|
|
23
|
+
CAN_ERASE_FLASH4 = "yes (needs firmware 3.2.2 or later)"
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class EpromSpecBuilder:
|
|
@@ -524,6 +526,11 @@ class EpromSpecBuilder:
|
|
|
524
526
|
etype, eprom_data.get("protocol-id"), eprom_data.get("pin-map")
|
|
525
527
|
):
|
|
526
528
|
output_data["can_erase_str"] = CAN_ERASE_YES
|
|
529
|
+
elif is_electrically_erasable(etype) and (
|
|
530
|
+
eprom_data.get("protocol-id") == FLASH4_PROTOCOL_ID
|
|
531
|
+
):
|
|
532
|
+
# `erase` sets FLAG_CAN_ERASE for flash4 itself (flash4_erase_gate).
|
|
533
|
+
output_data["can_erase_str"] = CAN_ERASE_FLASH4
|
|
527
534
|
elif is_electrically_erasable(etype):
|
|
528
535
|
output_data["can_erase_str"] = CAN_ERASE_NOT_SUPPORTED
|
|
529
536
|
elif etype == "UV-EPROM":
|
|
@@ -20,6 +20,7 @@ import serial.serialutil
|
|
|
20
20
|
import serial.tools.list_ports
|
|
21
21
|
|
|
22
22
|
import firestarter.codec as codec
|
|
23
|
+
import firestarter.flash4_erase_gate as flash4_erase_gate
|
|
23
24
|
import firestarter.transport_counters as transport_counters
|
|
24
25
|
from firestarter.config import ConfigManager # Assuming ConfigManager is refactored
|
|
25
26
|
from firestarter.constants import (
|
|
@@ -957,9 +958,17 @@ class SerialCommunicator:
|
|
|
957
958
|
# to major=3 later. Set FIRESTARTER_DEV_ALLOW_PRE_V12=1 to bypass when # noqa: E501
|
|
958
959
|
# bench-testing a current host against a historical (v2.x) firmware build. # noqa: E501
|
|
959
960
|
SerialCommunicator._validate_firmware_version(
|
|
960
|
-
version_match.group(0),
|
|
961
|
+
version_match.group(0).rstrip("."),
|
|
961
962
|
allow_pre_v12=os.environ.get("FIRESTARTER_DEV_ALLOW_PRE_V12") == "1",
|
|
962
963
|
)
|
|
964
|
+
# A flash4 erase on firmware older than 3.2.2 reaches either a
|
|
965
|
+
# 12 V erase (3.0.0b8-b26) or a "not supported" answer.
|
|
966
|
+
if flash4_erase_gate.needs_newer_firmware(command_to_send, identity):
|
|
967
|
+
raise FirmwareOutdatedError(
|
|
968
|
+
flash4_erase_gate.firmware_refusal_text(
|
|
969
|
+
identity.split(":")[0] if identity else None
|
|
970
|
+
)
|
|
971
|
+
)
|
|
963
972
|
|
|
964
973
|
# Shield-revision gate — ordered after the version check because
|
|
965
974
|
# firmware old enough to fail that check cannot be trusted to have
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firestarter
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.6
|
|
4
4
|
Summary: EPROM programmer software for the Relatively Universal ROM Programmer
|
|
5
5
|
Author-email: Henrik Olsson <henols@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/henols/firestarter_app
|
|
@@ -40,10 +40,10 @@ class TestFormatMessageRevision:
|
|
|
40
40
|
assert result == "Rev 0"
|
|
41
41
|
|
|
42
42
|
def test_msg_ok_rev_with_override(self):
|
|
43
|
-
"""MSG_OK_REV: effective!=0xFF → '{eff_str},
|
|
43
|
+
"""MSG_OK_REV: effective!=0xFF → '{eff_str}, Detected HW: {phys_str}'."""
|
|
44
44
|
entry = CATALOG[MSG_OK_REV]
|
|
45
45
|
result = format_message(MSG_OK_REV, [0x02, 0x04], entry)
|
|
46
|
-
assert result == "Rev 2.2 (override),
|
|
46
|
+
assert result == "Rev 2.2 (override), Detected HW: Rev 2.0-class"
|
|
47
47
|
|
|
48
48
|
def test_msg_ok_cfg_no_override(self):
|
|
49
49
|
"""MSG_OK_CFG: override==0xFF → 'R1: {r1}, R2: {r2}'."""
|
|
@@ -32,8 +32,8 @@ def test_format_msg_ok_rev_with_override_includes_both() -> None:
|
|
|
32
32
|
MSG_OK_REV, [REVISION_2_2, REVISION_UNKNOWN], CATALOG[MSG_OK_REV]
|
|
33
33
|
)
|
|
34
34
|
assert out is not None
|
|
35
|
-
# The format is "{eff},
|
|
36
|
-
assert "
|
|
35
|
+
# The format is "{eff}, Detected HW: {phys}"
|
|
36
|
+
assert "Detected HW" in out
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def test_format_msg_ok_cfg_no_override() -> None:
|
|
@@ -363,8 +363,8 @@ class TestIdFrameDecoder:
|
|
|
363
363
|
|
|
364
364
|
def test_ok_rev_p02_with_override_decodes(self, fake_serial, make_comm):
|
|
365
365
|
"""P-02: MSG_OK_REV with physical=0x01, effective=0x02 (override active)
|
|
366
|
-
renders 'Rev 2.0-class,
|
|
367
|
-
|
|
366
|
+
renders 'Rev 2.0-class, Detected HW: Rev 1': the override, then the
|
|
367
|
+
revision the board detected."""
|
|
368
368
|
comm = make_comm()
|
|
369
369
|
# params: u8 physical=0x01, u8 effective=0x02
|
|
370
370
|
params = bytes([0x01, 0x02])
|
|
@@ -374,7 +374,7 @@ class TestIdFrameDecoder:
|
|
|
374
374
|
response = _drive_one_response(comm)
|
|
375
375
|
assert response is not None
|
|
376
376
|
assert response.type == "OK"
|
|
377
|
-
assert response.message == "Rev 2.0-class,
|
|
377
|
+
assert response.message == "Rev 2.0-class, Detected HW: Rev 1"
|
|
378
378
|
|
|
379
379
|
def test_ok_rev_p02_no_override_decodes(self, fake_serial, make_comm):
|
|
380
380
|
"""P-02: MSG_OK_REV with physical=0x01, effective=0xFF sentinel renders
|
|
@@ -370,38 +370,42 @@ class TestFirmwareInstallPreRelease:
|
|
|
370
370
|
assert version == "3.1.0rc1"
|
|
371
371
|
assert "uno_rc1.hex" in url
|
|
372
372
|
|
|
373
|
-
def
|
|
374
|
-
"""
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
373
|
+
def test_pre_takes_stable_when_no_prerelease(self, monkeypatch):
|
|
374
|
+
"""With no pre-release in the list, the pre channel takes the stable one."""
|
|
375
|
+
monkeypatch.setattr(
|
|
376
|
+
firmware.requests,
|
|
377
|
+
"get",
|
|
378
|
+
lambda url, **kw: mock_releases_factory([_STABLE_RELEASE_UNO]),
|
|
379
|
+
)
|
|
380
|
+
fm = FirmwareManager(config_manager=MagicMock())
|
|
381
|
+
version, url = fm.fetch_release_info(channel="pre", board="uno")
|
|
382
|
+
assert version == "3.0.0"
|
|
383
|
+
assert url == "https://example.com/uno_stable.hex"
|
|
384
384
|
|
|
385
|
-
|
|
385
|
+
def test_pre_takes_a_newer_stable_over_an_older_prerelease(self, monkeypatch):
|
|
386
|
+
"""Like `pip --pre`: stable 3.2.2 wins over pre-release 3.1.1b2."""
|
|
386
387
|
|
|
387
|
-
def
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
388
|
+
def release(tag, prerelease):
|
|
389
|
+
return {
|
|
390
|
+
"tag_name": tag,
|
|
391
|
+
"prerelease": prerelease,
|
|
392
|
+
"draft": False,
|
|
393
|
+
"assets": [
|
|
394
|
+
{
|
|
395
|
+
"name": "firestarter_uno.hex",
|
|
396
|
+
"browser_download_url": f"https://example.com/{tag}.hex",
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
}
|
|
392
400
|
|
|
393
|
-
|
|
401
|
+
releases = [release("3.2.2", False), release("3.1.1b2", True)]
|
|
402
|
+
monkeypatch.setattr(
|
|
403
|
+
firmware.requests, "get", lambda url, **kw: mock_releases_factory(releases)
|
|
404
|
+
)
|
|
394
405
|
fm = FirmwareManager(config_manager=MagicMock())
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
assert version == "3.0.0"
|
|
399
|
-
assert url is not None
|
|
400
|
-
# Must log something about falling back to stable
|
|
401
|
-
lower_records = [r.message.lower() for r in caplog.records]
|
|
402
|
-
assert any(
|
|
403
|
-
"fall" in m or "stable" in m or "no pre" in m for m in lower_records
|
|
404
|
-
), f"Expected a fallback log record; got: {[r.message for r in caplog.records]}"
|
|
406
|
+
version, url = fm.fetch_release_info(channel="pre", board="uno")
|
|
407
|
+
assert version == "3.2.2"
|
|
408
|
+
assert url == "https://example.com/3.2.2.hex"
|
|
405
409
|
|
|
406
410
|
def test_pre_filters_draft_releases(self, monkeypatch):
|
|
407
411
|
"""INST-02 / D-03 — draft releases are excluded from pre-release candidates.
|
|
@@ -4,38 +4,25 @@ Copyright (c) 2024 Henrik Olsson
|
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted under MIT license.
|
|
6
6
|
|
|
7
|
-
Flash4 (protocol 0x05) erase
|
|
7
|
+
Flash4 (protocol 0x05) erase gate.
|
|
8
|
+
|
|
9
|
+
Firmware 3.2.2 erases a 0x05 part with a 5 V software chip erase. Older
|
|
10
|
+
firmware either answers "not supported" or, in 3.0.0b8 to 3.0.0b26, erases
|
|
11
|
+
with VPP and VPE on: 12 V on a 5 V part.
|
|
8
12
|
|
|
9
13
|
Coverage:
|
|
10
|
-
1.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`resolve_chip` against `EpromDatabase(skip_local_override=True)`, is
|
|
20
|
-
exactly the set of shipped rows whose `programming.algorithm` is 5.
|
|
21
|
-
4. NO PART-NUMBER LITERAL IN THE MODULE -- no shipped `part_number` string
|
|
22
|
-
occurs anywhere in `flash4_erase_gate.py`'s source, docstrings
|
|
23
|
-
included, proving the predicate is database-derived rather than a
|
|
24
|
-
hand-kept list.
|
|
25
|
-
5. THE MESSAGE SHAPE -- the rendered refusal line is exactly one
|
|
26
|
-
line, names the chip, and carries none of a forbidden-substring list
|
|
27
|
-
covering the cause, the alternative, and the `--force` workaround.
|
|
28
|
-
6. NOT A BLANKET REFUSAL -- a part whose algorithm is not 5 still
|
|
29
|
-
reaches `EpromOperator.erase_eprom`.
|
|
30
|
-
|
|
31
|
-
End-to-end proof that `firestarter erase` on a flash4 part refuses before
|
|
32
|
-
the serial port opens, printing exactly one line and never reaching
|
|
33
|
-
`EpromOperator.erase_eprom` -- the pre-connect guarantee.
|
|
14
|
+
1. `erase` on a flash4 part reaches the operator with FLAG_CAN_ERASE set,
|
|
15
|
+
and the database still leaves the flag clear, so `write` never has it.
|
|
16
|
+
2. `needs_newer_firmware`: refused below 3.2.2 and for an unparseable
|
|
17
|
+
version, accepted from 3.2.2 (and its pre-release and dev builds), and
|
|
18
|
+
never applied to another command or another protocol.
|
|
19
|
+
3. `setup_command` raises before the operation starts when a flash4 erase
|
|
20
|
+
meets firmware older than 3.2.2.
|
|
21
|
+
4. `is_flash4` over every shipped `algorithm` value, its fail-open polarity,
|
|
22
|
+
and its coupling to the real database.
|
|
34
23
|
"""
|
|
35
24
|
|
|
36
|
-
import
|
|
37
|
-
from pathlib import Path
|
|
38
|
-
from unittest.mock import Mock, patch
|
|
25
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
39
26
|
|
|
40
27
|
import pytest
|
|
41
28
|
from click.testing import CliRunner
|
|
@@ -44,13 +31,26 @@ from firestarter import cli_handlers
|
|
|
44
31
|
from firestarter.chip_resolver import resolve_chip
|
|
45
32
|
from firestarter.cli_handlers import AppContext, cli
|
|
46
33
|
from firestarter.config import ConfigManager
|
|
34
|
+
from firestarter.constants import COMMAND_ERASE, COMMAND_WRITE, FLAG_CAN_ERASE
|
|
47
35
|
from firestarter.database import EpromDatabase
|
|
48
36
|
from firestarter.eprom_info import EpromConsolePresenter
|
|
49
37
|
from firestarter.eprom_operations import EpromOperator
|
|
50
|
-
from firestarter.exceptions import
|
|
38
|
+
from firestarter.exceptions import (
|
|
39
|
+
ChipNotFoundError,
|
|
40
|
+
ChipNotImplementedError,
|
|
41
|
+
FirmwareOutdatedError,
|
|
42
|
+
)
|
|
51
43
|
from firestarter.firmware import FirmwareManager
|
|
52
|
-
from firestarter.flash4_erase_gate import
|
|
44
|
+
from firestarter.flash4_erase_gate import (
|
|
45
|
+
FLASH4_PROTOCOL_ID,
|
|
46
|
+
firmware_refusal_text,
|
|
47
|
+
is_flash4,
|
|
48
|
+
needs_newer_firmware,
|
|
49
|
+
)
|
|
53
50
|
from firestarter.hardware import HardwareManager
|
|
51
|
+
from firestarter.messages import MSG_OK_READY
|
|
52
|
+
|
|
53
|
+
from .conftest import build_frame
|
|
54
54
|
|
|
55
55
|
NO_PIN1_BUS_CONFIG = {"bus": [0, 1, 2]}
|
|
56
56
|
|
|
@@ -66,51 +66,88 @@ def _cli_app_context(eprom_operator):
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def test_cli_erase_on_flash4_part_sends_can_erase():
|
|
70
70
|
runner = CliRunner()
|
|
71
71
|
eprom_operator = Mock(spec=EpromOperator)
|
|
72
|
+
eprom_operator.erase_eprom.return_value = True
|
|
72
73
|
app = _cli_app_context(eprom_operator)
|
|
73
74
|
with patch.object(
|
|
74
75
|
cli_handlers,
|
|
75
76
|
"resolve_chip",
|
|
76
|
-
return_value={"algorithm": 5, "bus-config": NO_PIN1_BUS_CONFIG},
|
|
77
|
+
return_value={"algorithm": 5, "flags": 0, "bus-config": NO_PIN1_BUS_CONFIG},
|
|
77
78
|
):
|
|
78
79
|
result = runner.invoke(cli, ["erase", "AE29F2008"], obj=app)
|
|
79
80
|
|
|
80
|
-
assert result.exit_code ==
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
assert result.exit_code == 0, result.output
|
|
82
|
+
eprom_operator.erase_eprom.assert_called_once()
|
|
83
|
+
sent = eprom_operator.erase_eprom.call_args.args[1]
|
|
84
|
+
assert sent["flags"] & FLAG_CAN_ERASE
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
def
|
|
87
|
-
|
|
87
|
+
def test_database_leaves_can_erase_clear_for_flash4_writes():
|
|
88
|
+
"""`write` must never carry FLAG_CAN_ERASE for a flash4 part."""
|
|
89
|
+
db = EpromDatabase(skip_local_override=True)
|
|
90
|
+
data = resolve_chip("AE29F2008", db=db)
|
|
91
|
+
assert is_flash4(data)
|
|
92
|
+
assert not data["flags"] & FLAG_CAN_ERASE
|
|
88
93
|
|
|
89
|
-
exit1_operator = Mock(spec=EpromOperator)
|
|
90
|
-
exit1_app = _cli_app_context(exit1_operator)
|
|
91
|
-
with patch.object(
|
|
92
|
-
cli_handlers,
|
|
93
|
-
"resolve_chip",
|
|
94
|
-
return_value={"algorithm": 5, "bus-config": NO_PIN1_BUS_CONFIG},
|
|
95
|
-
):
|
|
96
|
-
exit1_result = runner.invoke(cli, ["erase", "AE29F2008"], obj=exit1_app)
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cli_handlers,
|
|
102
|
-
"resolve_chip",
|
|
103
|
-
return_value={"algorithm": 5, "bus-config": NO_PIN1_BUS_CONFIG},
|
|
104
|
-
):
|
|
105
|
-
exit0_result = runner.invoke(
|
|
106
|
-
cli, ["erase", "AE29F2008", "--ignore-unsupported"], obj=exit0_app
|
|
107
|
-
)
|
|
95
|
+
def _erase(flags=FLAG_CAN_ERASE, algorithm=FLASH4_PROTOCOL_ID, cmd=COMMAND_ERASE):
|
|
96
|
+
return {"cmd": cmd, "algorithm": algorithm, "flags": flags}
|
|
97
|
+
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
99
|
+
@pytest.mark.parametrize(
|
|
100
|
+
"version",
|
|
101
|
+
[None, "", "garbage", "3.0.0", "3.0.0b20", "3.1.1b2", "3.2.0", "3.2.1"],
|
|
102
|
+
)
|
|
103
|
+
def test_flash4_erase_needs_firmware_3_2_2(version):
|
|
104
|
+
assert needs_newer_firmware(_erase(), version) is True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize(
|
|
108
|
+
"version",
|
|
109
|
+
["3.2.2", "3.2.2:uno", "3.2.2rc1:leonardo", "3.2.2.dev48", "3.2.10", "3.3.0"],
|
|
110
|
+
)
|
|
111
|
+
def test_flash4_erase_runs_on_firmware_3_2_2_and_later(version):
|
|
112
|
+
assert needs_newer_firmware(_erase(), version) is False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize(
|
|
116
|
+
"command",
|
|
117
|
+
[
|
|
118
|
+
_erase(algorithm=6),
|
|
119
|
+
_erase(cmd=COMMAND_WRITE),
|
|
120
|
+
_erase(flags=0),
|
|
121
|
+
{"state": 13},
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
def test_firmware_gate_applies_only_to_a_flagged_flash4_erase(command):
|
|
125
|
+
assert needs_newer_firmware(command, "3.0.0b20") is False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_firmware_refusal_text_is_one_line_and_names_the_versions():
|
|
129
|
+
text = firmware_refusal_text("3.2.1")
|
|
130
|
+
assert "\n" not in text
|
|
131
|
+
assert "3.2.2" in text and "3.2.1" in text
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _ready_frame(version: bytes) -> bytes:
|
|
135
|
+
# [buffer_size u16][hw_revision u8][ver_len u8][version bytes]
|
|
136
|
+
params = b"\x02\x00" + bytes([0]) + bytes([len(version)]) + version
|
|
137
|
+
return build_frame(MSG_OK_READY, params)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_setup_command_refuses_flash4_erase_on_old_firmware(make_comm, fake_serial):
|
|
141
|
+
fake_serial.feed(_ready_frame(b"3.2.1:uno"))
|
|
142
|
+
comm = make_comm()
|
|
143
|
+
with pytest.raises(FirmwareOutdatedError, match="3.2.2"):
|
|
144
|
+
comm.setup_command(_erase(), MagicMock())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_setup_command_accepts_flash4_erase_on_3_2_2(make_comm, fake_serial):
|
|
148
|
+
fake_serial.feed(_ready_frame(b"3.2.2:uno"))
|
|
149
|
+
comm = make_comm()
|
|
150
|
+
assert comm.setup_command(_erase(), MagicMock()) is True
|
|
114
151
|
|
|
115
152
|
|
|
116
153
|
def _shipped_algorithm_values(db: EpromDatabase) -> set:
|
|
@@ -188,47 +225,6 @@ def test_predicate_matches_real_database_algorithm_5_rows_exactly():
|
|
|
188
225
|
)
|
|
189
226
|
|
|
190
227
|
|
|
191
|
-
def _module_source(module) -> str:
|
|
192
|
-
path = inspect.getsourcefile(module)
|
|
193
|
-
assert path is not None, f"could not resolve a source file for {module!r}"
|
|
194
|
-
return Path(path).read_text(encoding="utf-8")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
FORBIDDEN_REFUSAL_SUBSTRINGS = (
|
|
198
|
-
"force",
|
|
199
|
-
"firestarter write",
|
|
200
|
-
"write",
|
|
201
|
-
"workaround",
|
|
202
|
-
"route around",
|
|
203
|
-
"because",
|
|
204
|
-
"reason",
|
|
205
|
-
"cause",
|
|
206
|
-
"alternative",
|
|
207
|
-
"instead",
|
|
208
|
-
"try",
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def test_refusal_text_is_one_line_and_carries_no_forbidden_content():
|
|
213
|
-
"""THE MESSAGE SHAPE: exactly one line, names the chip, and
|
|
214
|
-
carries none of a forbidden-substring list covering the cause, the
|
|
215
|
-
alternative, and the `--force` forged-identity workaround. A NEGATIVE
|
|
216
|
-
assertion on purpose -- it is what stops a later executor quietly
|
|
217
|
-
re-adding the cause clause and the alternative that were deliberately
|
|
218
|
-
left out of the tool's output.
|
|
219
|
-
"""
|
|
220
|
-
text = refusal_text("ae29f2008")
|
|
221
|
-
|
|
222
|
-
assert "\n" not in text
|
|
223
|
-
assert text == "Erase not supported for AE29F2008"
|
|
224
|
-
|
|
225
|
-
lowered = text.lower()
|
|
226
|
-
for forbidden in FORBIDDEN_REFUSAL_SUBSTRINGS:
|
|
227
|
-
assert forbidden not in lowered, (
|
|
228
|
-
f"refusal text {text!r} contains forbidden substring {forbidden!r}"
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
228
|
def test_cli_erase_on_non_flash4_part_still_reaches_operator():
|
|
233
229
|
"""NOT A BLANKET REFUSAL: a part whose algorithm is not 5 still
|
|
234
230
|
reaches `EpromOperator.erase_eprom`. Without this leg, a fail-closed
|