firestarter 3.2.0b1__tar.gz → 3.2.1__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.0b1/firestarter.egg-info → firestarter-3.2.1}/PKG-INFO +2 -1
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/_version.py +3 -3
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/chip_resolver.py +12 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/cli_handlers.py +88 -15
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/codec.py +29 -7
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/constants.py +12 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/diagnostic_report.py +4 -4
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/eprom_operations.py +65 -31
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/firmware.py +3 -2
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/hardware.py +44 -2
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/serial_comm.py +44 -11
- firestarter-3.2.1/firestarter/timestamps.py +83 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1/firestarter.egg-info}/PKG-INFO +2 -1
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter.egg-info/SOURCES.txt +5 -9
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter.egg-info/requires.txt +1 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter.egg-info/scm_file_list.json +5 -9
- firestarter-3.2.1/firestarter.egg-info/scm_version.json +8 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/pyproject.toml +1 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/__snapshots__/test_characterization.ambr +15 -12
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/conftest.py +36 -0
- firestarter-3.2.0b1/tests/golden/wire_dict_baseline.json → firestarter-3.2.1/tests/golden/wire_dict.json +189 -137
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_cli_handlers.py +2 -2
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_codec_format_message.py +62 -0
- firestarter-3.2.1/tests/test_config_band_refusal.py +188 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_consistency_check.py +7 -1
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_derive_plan_no_drop_sweep.py +14 -22
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_devtest_firmware_error_propagation.py +11 -4
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_eprom_operations.py +123 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_erase_flag_invariants.py +20 -30
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_firmware_install.py +4 -2
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_hardware.py +56 -2
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_page_size_invariants.py +22 -44
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_probe_spurious_setup_ack.py +3 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_protocol_not_implemented_production_path.py +4 -1
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_sdp_db_invariant.py +22 -62
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_session_lease.py +52 -1
- firestarter-3.2.1/tests/test_setup_refusals.py +229 -0
- firestarter-3.2.1/tests/test_timestamps.py +87 -0
- firestarter-3.2.1/tests/test_wire_dict_equivalence.py +177 -0
- firestarter-3.2.0b1/firestarter.egg-info/scm_version.json +0 -8
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_149.json +0 -65
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_153.json +0 -265
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_182.json +0 -219
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_194.json +0 -86
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_197.json +0 -20
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_198.json +0 -17
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_199.json +0 -14
- firestarter-3.2.0b1/tests/golden/wire_dict_expected_deltas_dip24_2816.json +0 -91
- firestarter-3.2.0b1/tests/test_wire_dict_equivalence.py +0 -821
- {firestarter-3.2.0b1 → firestarter-3.2.1}/LICENSE +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/MANIFEST.in +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/README.md +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/__init__.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/address_parser.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/avr_tool.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/channel.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/chip_test.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/compare.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/config.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/data/chip_database.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/data/pinouts.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/database.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/dip24_2816_erase_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/eprom_info.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/erase_support.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/exceptions.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/flash4_erase_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/frame_parser.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/fw_release_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/ic_layout.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/jp5_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/jumper_table.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/lock_status.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/log_capture.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/logging_utils.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/main.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/messages.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/page_size_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/protection_readability.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/py32_dfu.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/sdp_capability.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/sdp_honesty.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/submit.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/transport_counters.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/utils.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/vpp_display.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter/write_blank_guard.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter.egg-info/dependency_links.txt +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter.egg-info/entry_points.txt +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/firestarter.egg-info/top_level.txt +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/setup.cfg +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/__init__.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fake_chip.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/part_number_delta.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/report_shapes.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/at28c256-full-all-ok-sdp.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/attr01-status-axis-transport-fault.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/gh20-at28c256-fail.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/gh23-w27e257-fail.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/gh28-m27c512-fail.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/gh47-sst27sf512-pass.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/m27c512-full-all-ok.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/m27c512-full-blank-check-bad.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/m27c512-full-canonical-name.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/m27c512-full-comma-joined-name.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/m27c512-full-runs-1.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/prune03-synthesized-fingerprint-match.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/sst27sf512-full-all-ok.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/sst27sf512-six-step-readback-gated.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/sst27sf512-six-step.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/synthetic-arm4-empty-results.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/synthetic-arm4-no-ok.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/uv-slot-write-pass.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/reports/w27e257-full-all-ok.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/shape_ids.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/fixtures/synthetic_nonzero_chip_id.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/golden/build_db_part_specific_constants.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/golden/chip_database_field_inventory.json +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/plan_corpus.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_address_parser.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_b15_page_size_corroboration.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_blast_radius_invariance.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_boot_block_hint.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_budget_failure_render.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_bug_characterization.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_build_db_constant_census.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_build_db_inclusion.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_build_db_interpret_timing.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_build_db_pinout_fork.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_canonical_part_number.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_characterization.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_database_field_inventory.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_resolver.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_test.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_test_blank_check_order.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_test_cycle.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_test_sdp_leg.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_test_timing.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_chip_test_uv_slot_write.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_click_group_gate_hook.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_cobs.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_codec.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_compare.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_config.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_connect_cost_harness.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_coverage_floor_v18.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_database_conversion.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_datasheet_overrides.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_decoder.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_derive_plan_structural_sentinel.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_dev_group_channel_gating.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_dev_test_cmd.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_dev_tools_channel_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_dfu_opcode_anchors.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_diagnostic_report.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_dip24_2816_erase_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_endpoint_constants.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_eprom_database.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_eprom_info.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_erase_blank_step_nonregression.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_error_code_seam.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_even_block.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_extra_chips_supplement.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_flash4_erase_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fw_list_failure_vs_empty.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fw_port_targeting_and_blind_install.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fw_release_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fw_update_dead_endpoint.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fw_update_path_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fw_version_guard.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_fwguard.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_hw_revision_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_ic_layout.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_info_vpp_and_erase.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_jp5_gate.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_jumper_table.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_lock_status_class_partition.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_lock_status_cli.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_lock_status_wire.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_log_capture.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_logging_utils.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_matrix_artifact.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_page_size_alignment_refusal.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_page_size_write_refusal.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_parse_devtest_issue.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_programming_vcc_census.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_protect_flags_doc_measurements.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_protection_resolution.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_protection_status_catalog.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_protection_table_citations.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_protocol_not_implemented.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_provenance.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_pulse_us_override.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_py32_channel_gating.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_py32_dfu.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_py32_flash_map_host.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_py32_pyusb_absent.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_pyusb_api_surface.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_sdp_capability.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_sdp_honesty.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_serial_characterization.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_serial_comm.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_submit.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_transport_counters.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_utils.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_uv_mask.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_validate_family_cmd.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_validate_oracle.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_validated_parts_regression_surface.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_vcc_margin_rail.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_vpp_decode_table.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_vpp_rail_classification.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_blank_guard.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_blank_guard_pinning.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_guard_port_pinning.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_progress.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_response_budget.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_skip_erase_0x0d.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/tests/test_write_skip_sdp_unlock.py +0 -0
- {firestarter-3.2.0b1 → firestarter-3.2.1}/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.1
|
|
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
|
|
@@ -37,6 +37,7 @@ Requires-Dist: syrupy<7,>=5.0; extra == "test"
|
|
|
37
37
|
Requires-Dist: ruff>=0.15.14; extra == "test"
|
|
38
38
|
Requires-Dist: mypy<3,>=2.1.0; extra == "test"
|
|
39
39
|
Requires-Dist: pytest-cov>=7.1.0; extra == "test"
|
|
40
|
+
Requires-Dist: pytest-xdist>=3.6; extra == "test"
|
|
40
41
|
Requires-Dist: types-pyserial>=3.5.0.20260519; extra == "test"
|
|
41
42
|
Dynamic: license-file
|
|
42
43
|
|
|
@@ -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.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 2, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g4526a89'
|
|
@@ -12,6 +12,8 @@ from typing import Any
|
|
|
12
12
|
from firestarter.database import EpromDatabase
|
|
13
13
|
from firestarter.exceptions import ChipNotFoundError, ChipNotImplementedError
|
|
14
14
|
|
|
15
|
+
_NON_ZERO_WIRE_FIELDS = ("memory-size", "pin-count", "vpp_mv")
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
|
|
17
19
|
"""Resolve a chip name to its programmer-config dict.
|
|
@@ -74,4 +76,14 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
|
|
|
74
76
|
data = db.convert_to_programmer(full) if full else None
|
|
75
77
|
if not data:
|
|
76
78
|
raise ChipNotFoundError(name)
|
|
79
|
+
|
|
80
|
+
# The firmware refuses a memory command whose memory-size, pin-count or
|
|
81
|
+
# vpp_mv is 0, with the same 'Bad JSON' answer that old firmware gives.
|
|
82
|
+
# No shipped row has a 0 here; a user database override can.
|
|
83
|
+
zero_fields = [key for key in _NON_ZERO_WIRE_FIELDS if not data.get(key)]
|
|
84
|
+
if zero_fields:
|
|
85
|
+
raise ChipNotImplementedError(
|
|
86
|
+
f"{name}: the chip entry gives 0 for {', '.join(zero_fields)}. "
|
|
87
|
+
"The programmer cannot use a value of 0 there."
|
|
88
|
+
)
|
|
77
89
|
return data
|
|
@@ -4,7 +4,6 @@ This module is the production CLI surface; main.py re-exports `cli` as `main`
|
|
|
4
4
|
for the console-script entry point.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import datetime
|
|
8
7
|
import functools
|
|
9
8
|
import hashlib
|
|
10
9
|
import json
|
|
@@ -60,6 +59,10 @@ from firestarter.chip_test import (
|
|
|
60
59
|
)
|
|
61
60
|
from firestarter.config import ConfigManager, get_config_dir
|
|
62
61
|
from firestarter.constants import (
|
|
62
|
+
CONFIG_R1_MAX,
|
|
63
|
+
CONFIG_R1_MIN,
|
|
64
|
+
CONFIG_R2_MAX,
|
|
65
|
+
CONFIG_R2_MIN,
|
|
63
66
|
FIRESTARTER_RELEASES_URL,
|
|
64
67
|
FLAG_CHIP_ENABLE,
|
|
65
68
|
FLAG_OUTPUT_ENABLE,
|
|
@@ -102,6 +105,7 @@ from firestarter.protection_readability import (
|
|
|
102
105
|
protection_gate_for_entry,
|
|
103
106
|
)
|
|
104
107
|
from firestarter.sdp_capability import SDP_PROTOCOL_ID, sdp_capability
|
|
108
|
+
from firestarter.timestamps import iso_cet
|
|
105
109
|
|
|
106
110
|
logger = logging.getLogger("Firestarter")
|
|
107
111
|
|
|
@@ -564,6 +568,9 @@ def read(
|
|
|
564
568
|
) -> None:
|
|
565
569
|
"""Reads the content from an EPROM."""
|
|
566
570
|
eprom_data = resolve_chip(eprom, db=app.db)
|
|
571
|
+
refusal = _address_size_refusal_exit_code(eprom=eprom, address=address, size=size)
|
|
572
|
+
if refusal is not None:
|
|
573
|
+
sys.exit(refusal)
|
|
567
574
|
ok = app.eprom_operator.read_eprom(
|
|
568
575
|
eprom,
|
|
569
576
|
eprom_data,
|
|
@@ -999,6 +1006,41 @@ def write(
|
|
|
999
1006
|
sys.exit(1)
|
|
1000
1007
|
|
|
1001
1008
|
|
|
1009
|
+
_SIZE_TOO_SMALL_REFUSAL = "{eprom}: refused -- the size {size!r} is less than 1 byte."
|
|
1010
|
+
_EMPTY_INPUT_FILE_REFUSAL = "{eprom}: refused -- the input file is empty."
|
|
1011
|
+
_NEGATIVE_SECTOR_ADDRESS_REFUSAL = (
|
|
1012
|
+
"{eprom}: refused -- the sector address {address!r} is negative."
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _address_size_refusal_exit_code(
|
|
1017
|
+
*, eprom: str, address: str | None, size: str | None
|
|
1018
|
+
) -> int | None:
|
|
1019
|
+
"""Refuse a negative start address or a size below 1 byte before the
|
|
1020
|
+
serial port opens.
|
|
1021
|
+
|
|
1022
|
+
The firmware refuses both at setup, with the same 'Bad JSON' answer that
|
|
1023
|
+
firmware too old for this CLI gives, so without this check the operator
|
|
1024
|
+
sees only "no programmer found". Returns 2 to refuse with, or `None`.
|
|
1025
|
+
A malformed string is not refused here: `_setup_operation`'s own parse
|
|
1026
|
+
handling still owns that error.
|
|
1027
|
+
"""
|
|
1028
|
+
try:
|
|
1029
|
+
write_blank_guard.require_non_negative_address(eprom, address)
|
|
1030
|
+
except NegativeStartAddressError as e:
|
|
1031
|
+
click.echo(str(e))
|
|
1032
|
+
return 2
|
|
1033
|
+
if size is not None:
|
|
1034
|
+
try:
|
|
1035
|
+
parsed_size = parse_size(size)
|
|
1036
|
+
except ValueError:
|
|
1037
|
+
return None
|
|
1038
|
+
if parsed_size is not None and parsed_size < 1:
|
|
1039
|
+
click.echo(_SIZE_TOO_SMALL_REFUSAL.format(eprom=eprom.upper(), size=size))
|
|
1040
|
+
return 2
|
|
1041
|
+
return None
|
|
1042
|
+
|
|
1043
|
+
|
|
1002
1044
|
def _region_refusal_exit_code(
|
|
1003
1045
|
*,
|
|
1004
1046
|
eprom: str,
|
|
@@ -1040,7 +1082,14 @@ def _region_refusal_exit_code(
|
|
|
1040
1082
|
also exit 2, via the existing setup-failure path. Returning `None` here
|
|
1041
1083
|
lets that path run rather than parsing the string a second time with
|
|
1042
1084
|
different error handling.
|
|
1085
|
+
|
|
1086
|
+
A negative start address, a size below 1 byte and an empty input file
|
|
1087
|
+
are refused here too: the firmware refuses a zero-length region at
|
|
1088
|
+
setup.
|
|
1043
1089
|
"""
|
|
1090
|
+
refusal = _address_size_refusal_exit_code(eprom=eprom, address=address, size=size)
|
|
1091
|
+
if refusal is not None:
|
|
1092
|
+
return refusal
|
|
1044
1093
|
try:
|
|
1045
1094
|
start = parse_address(address) if address is not None else 0
|
|
1046
1095
|
except ValueError:
|
|
@@ -1098,6 +1147,10 @@ def _region_refusal_exit_code(
|
|
|
1098
1147
|
else:
|
|
1099
1148
|
length = None
|
|
1100
1149
|
|
|
1150
|
+
if explicit_size is None and input_file is not None and length == 0:
|
|
1151
|
+
click.echo(_EMPTY_INPUT_FILE_REFUSAL.format(eprom=eprom.upper()))
|
|
1152
|
+
return 2
|
|
1153
|
+
|
|
1101
1154
|
if length is not None and mem_size is not None and start + length > mem_size:
|
|
1102
1155
|
click.echo(
|
|
1103
1156
|
f"{eprom.upper()}: refused -- the region 0x{start:X}-"
|
|
@@ -1193,8 +1246,9 @@ def verify(
|
|
|
1193
1246
|
Exits 0 on a match, 1 on a mismatch, 2 on a transport, hardware, setup,
|
|
1194
1247
|
or region failure. The three are distinct: a transport failure is not
|
|
1195
1248
|
reported as a mismatch, and a region refusal -- an explicit --size
|
|
1196
|
-
longer than the input file,
|
|
1197
|
-
is reported before the serial
|
|
1249
|
+
longer than the input file, a region running past the chip's end, a
|
|
1250
|
+
negative address, or an empty region -- is reported before the serial
|
|
1251
|
+
port ever opens.
|
|
1198
1252
|
|
|
1199
1253
|
Without --size, the compared region is the input file's own length;
|
|
1200
1254
|
with it, --size wins.
|
|
@@ -1255,8 +1309,9 @@ def blank(
|
|
|
1255
1309
|
Exits 0 when blank, 1 when at least one byte is not blank, 2 on a
|
|
1256
1310
|
transport, hardware, setup, or region failure. The three are distinct:
|
|
1257
1311
|
a transport failure is not reported as a not-blank verdict, and a
|
|
1258
|
-
region refusal -- a region running past the chip's end
|
|
1259
|
-
before the serial port ever
|
|
1312
|
+
region refusal -- a region running past the chip's end, a negative
|
|
1313
|
+
address, or a size of 0 -- is reported before the serial port ever
|
|
1314
|
+
opens.
|
|
1260
1315
|
|
|
1261
1316
|
Without --size, the checked region is the whole chip; with it, --size
|
|
1262
1317
|
wins.
|
|
@@ -1352,6 +1407,18 @@ def erase(
|
|
|
1352
1407
|
if sector_blank_refusal is not None:
|
|
1353
1408
|
sys.exit(sector_blank_refusal)
|
|
1354
1409
|
|
|
1410
|
+
try:
|
|
1411
|
+
negative_sector = (parse_address(sector_address) or 0) < 0
|
|
1412
|
+
except ValueError:
|
|
1413
|
+
negative_sector = False
|
|
1414
|
+
if negative_sector:
|
|
1415
|
+
click.echo(
|
|
1416
|
+
_NEGATIVE_SECTOR_ADDRESS_REFUSAL.format(
|
|
1417
|
+
eprom=eprom.upper(), address=sector_address
|
|
1418
|
+
)
|
|
1419
|
+
)
|
|
1420
|
+
sys.exit(2)
|
|
1421
|
+
|
|
1355
1422
|
eprom_data = resolve_chip(eprom, db=app.db)
|
|
1356
1423
|
|
|
1357
1424
|
if flash4_erase_gate.is_flash4(eprom_data):
|
|
@@ -1492,13 +1559,12 @@ def hw(app: AppContext) -> None:
|
|
|
1492
1559
|
|
|
1493
1560
|
|
|
1494
1561
|
_BANDGAP_NOMINAL_MV = 1100
|
|
1495
|
-
_DIVIDER_GAIN = 314000 / 44000
|
|
1496
1562
|
|
|
1497
1563
|
|
|
1498
1564
|
@cli.command(name="cal")
|
|
1499
1565
|
@click.option(
|
|
1500
1566
|
"--vcc",
|
|
1501
|
-
type=
|
|
1567
|
+
type=click.FloatRange(min=0, min_open=True),
|
|
1502
1568
|
default=None,
|
|
1503
1569
|
metavar="VOLTS",
|
|
1504
1570
|
help="Calibrate using a multimeter reading of the 5V pin, e.g. --vcc 5.09",
|
|
@@ -1545,7 +1611,9 @@ def cal(app: AppContext, vcc: float | None, reset: bool) -> None:
|
|
|
1545
1611
|
click.echo(
|
|
1546
1612
|
"Calibration refused. The result was outside 1000-1200 mV, which "
|
|
1547
1613
|
"means the reading does not match this board. Check that the "
|
|
1548
|
-
"multimeter is on the 5V pin and that the value is in volts."
|
|
1614
|
+
"multimeter is on the 5V pin and that the value is in volts. "
|
|
1615
|
+
"A py32f071 board has no bandgap calibration and always "
|
|
1616
|
+
"refuses it."
|
|
1549
1617
|
)
|
|
1550
1618
|
sys.exit(1)
|
|
1551
1619
|
after = app.hardware_manager.read_calibration()
|
|
@@ -1599,17 +1667,17 @@ def _echo_calibration_effect(bandgap_mv: int) -> None:
|
|
|
1599
1667
|
"-r1",
|
|
1600
1668
|
"--r16",
|
|
1601
1669
|
"r16",
|
|
1602
|
-
type=
|
|
1670
|
+
type=click.IntRange(CONFIG_R1_MIN, CONFIG_R1_MAX),
|
|
1603
1671
|
default=None,
|
|
1604
|
-
help="Set R16 resistance, resistor connected to VPE",
|
|
1672
|
+
help="Set R16 resistance in ohms, resistor connected to VPE.",
|
|
1605
1673
|
)
|
|
1606
1674
|
@click.option(
|
|
1607
1675
|
"-r2",
|
|
1608
1676
|
"--r14r15",
|
|
1609
1677
|
"r14r15",
|
|
1610
|
-
type=
|
|
1678
|
+
type=click.IntRange(CONFIG_R2_MIN, CONFIG_R2_MAX),
|
|
1611
1679
|
default=None,
|
|
1612
|
-
help="Set R14/R15 resistance, resistors connected to GND",
|
|
1680
|
+
help="Set R14/R15 resistance in ohms, resistors connected to GND.",
|
|
1613
1681
|
)
|
|
1614
1682
|
@click.pass_obj
|
|
1615
1683
|
@map_typed_errors
|
|
@@ -1623,6 +1691,8 @@ def config(
|
|
|
1623
1691
|
# --rev is an IntRange: '--rev 2.2' used to go through float() and int()
|
|
1624
1692
|
# and silently select code 2 (Rev 2.0). Click now refuses it before any
|
|
1625
1693
|
# serial byte is sent, and refuses a code the firmware does not define.
|
|
1694
|
+
# -r1 and -r2 are IntRanges for the same reason: the firmware refuses a
|
|
1695
|
+
# value outside its band, so the host refuses it before the port opens.
|
|
1626
1696
|
ok = app.hardware_manager.set_hardware_config(
|
|
1627
1697
|
rev, r16, r14r15, flags=_build_op_flags()
|
|
1628
1698
|
)
|
|
@@ -1838,10 +1908,10 @@ def fw(
|
|
|
1838
1908
|
|
|
1839
1909
|
print(_json.dumps(releases, indent=2))
|
|
1840
1910
|
else:
|
|
1841
|
-
print(f"{'Version':<12} {'Channel':<14} {'Published':<
|
|
1911
|
+
print(f"{'Version':<12} {'Channel':<14} {'Published':<25} Asset URL")
|
|
1842
1912
|
for r in releases:
|
|
1843
1913
|
print(
|
|
1844
|
-
f"{r['version']:<12} {r['channel']:<14} {r['published']:<
|
|
1914
|
+
f"{r['version']:<12} {r['channel']:<14} {r['published']:<25} {r['asset_url']}" # noqa: E501
|
|
1845
1915
|
)
|
|
1846
1916
|
if not releases:
|
|
1847
1917
|
print(f"No releases found for board {board}.")
|
|
@@ -1989,6 +2059,9 @@ def dev_read(
|
|
|
1989
2059
|
) -> None:
|
|
1990
2060
|
"""Reads the content from an EPROM and prints data to console."""
|
|
1991
2061
|
eprom_data = resolve_chip(eprom, db=app.db)
|
|
2062
|
+
refusal = _address_size_refusal_exit_code(eprom=eprom, address=address, size=size)
|
|
2063
|
+
if refusal is not None:
|
|
2064
|
+
sys.exit(refusal)
|
|
1992
2065
|
ok = app.eprom_operator.dev_read_eprom(
|
|
1993
2066
|
eprom,
|
|
1994
2067
|
eprom_data,
|
|
@@ -2565,7 +2638,7 @@ def _write_artifact(
|
|
|
2565
2638
|
out_path.mkdir(parents=True, exist_ok=True)
|
|
2566
2639
|
|
|
2567
2640
|
artifact: Dict[str, Any] = { # noqa: UP006
|
|
2568
|
-
"generated":
|
|
2641
|
+
"generated": iso_cet(),
|
|
2569
2642
|
"harness_version": "71",
|
|
2570
2643
|
"cells": cells,
|
|
2571
2644
|
}
|
|
@@ -40,6 +40,12 @@ from firestarter.messages import (
|
|
|
40
40
|
|
|
41
41
|
logger = logging.getLogger("Codec")
|
|
42
42
|
|
|
43
|
+
# Older, shorter forms of fixed-width acks that the host still accepts. The
|
|
44
|
+
# value is the legacy param-region length; the decoder reads only the leading
|
|
45
|
+
# catalog params that fit in it. MSG_OK_CFG gained the trailing u16 bandgap in
|
|
46
|
+
# 3.1.0b4, so the 3.1.0 stable firmware still sends the 9-byte form.
|
|
47
|
+
_LEGACY_PARAM_BYTES = {MSG_OK_CFG: 9}
|
|
48
|
+
|
|
43
49
|
# REVISION_* byte → silkscreen-string mapping for MSG_OK_REV rendering.
|
|
44
50
|
# Mirrors firmware enum at firestarter/include/rurp_shield.h. Lookup-via-dict.get()
|
|
45
51
|
# so unknown bytes fall back to "Rev{n}" instead of raising.
|
|
@@ -71,9 +77,13 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
|
|
|
71
77
|
effective!=0xFF → "Rev{effective}, Override HW: Rev{physical}"
|
|
72
78
|
|
|
73
79
|
P-03 MSG_OK_CFG — params[0]=r1 u32, params[1]=r2 u32, params[2]=override u8,
|
|
74
|
-
params[3]=bandgap_mv u16 (
|
|
75
|
-
override==0xFF → "R1: {r1}, R2: {r2}"
|
|
80
|
+
params[3]=bandgap_mv u16 (absent on firmware before 3.1.0b4)
|
|
81
|
+
override==0xFF → "R1: {r1}, R2: {r2}, Bandgap: {n} mV"
|
|
76
82
|
override!=0xFF → "R1: {r1}, R2: {r2}, Override HW: {silkscreen_str}, Bandgap: {n} mV"
|
|
83
|
+
The Bandgap clause is always present when the ack carries the field:
|
|
84
|
+
HardwareManager.read_calibration keys on it. The 9-byte form from older
|
|
85
|
+
firmware has no bandgap, so it renders without the Bandgap clause, and
|
|
86
|
+
read_calibration reports that firmware as too old.
|
|
77
87
|
Override clause now routes through
|
|
78
88
|
_REVISION_SILKSCREEN so the same byte that surfaces as "Rev 2.0-class"
|
|
79
89
|
via MSG_OK_REV no longer surfaces as "Rev2" on the adjacent ack line.
|
|
@@ -96,8 +106,8 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
|
|
|
96
106
|
eff_str = _REVISION_SILKSCREEN.get(effective, f"Rev{effective}")
|
|
97
107
|
return f"{eff_str}, Override HW: {phys_str}"
|
|
98
108
|
|
|
99
|
-
if msg_id == MSG_OK_CFG and len(params)
|
|
100
|
-
r1, r2, override
|
|
109
|
+
if msg_id == MSG_OK_CFG and len(params) in (3, 4):
|
|
110
|
+
r1, r2, override = params[0], params[1], params[2]
|
|
101
111
|
head = f"R1: {r1}, R2: {r2}"
|
|
102
112
|
if override != 0xFF:
|
|
103
113
|
# Route the override byte through
|
|
@@ -106,9 +116,12 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
|
|
|
106
116
|
# No-space "Rev{n}" fallback mirrors the MSG_OK_REV branch shape.
|
|
107
117
|
override_str = _REVISION_SILKSCREEN.get(override, f"Rev{override}")
|
|
108
118
|
head += f", Override HW: {override_str}"
|
|
119
|
+
if len(params) == 3:
|
|
120
|
+
# Firmware before 3.1.0b4 sends no bandgap field.
|
|
121
|
+
return head
|
|
109
122
|
# v1.43 appended the calibrated reference. Rendered last so the
|
|
110
123
|
# override clause keeps the position every prior release put it in.
|
|
111
|
-
return f"{head}, Bandgap: {
|
|
124
|
+
return f"{head}, Bandgap: {params[3]} mV"
|
|
112
125
|
|
|
113
126
|
# Silkscreen-aware rendering for the two
|
|
114
127
|
# boot-time INFO surfaces that carry the same revision byte as
|
|
@@ -230,18 +243,27 @@ def decode_id_frame(frame_len: int, body: bytes) -> LogMessage | None:
|
|
|
230
243
|
# entries carry param_bytes == -1 in the catalog; for those we
|
|
231
244
|
# cannot pre-validate, but _decode_param will surface any overrun
|
|
232
245
|
# via IndexError below.
|
|
233
|
-
|
|
246
|
+
legacy_len = _LEGACY_PARAM_BYTES.get(msg_id)
|
|
247
|
+
is_legacy = legacy_len is not None and len(params_bytes) == legacy_len
|
|
248
|
+
if (
|
|
249
|
+
entry.param_bytes >= 0
|
|
250
|
+
and len(params_bytes) != entry.param_bytes
|
|
251
|
+
and not is_legacy
|
|
252
|
+
):
|
|
234
253
|
logger.warning(
|
|
235
254
|
f"Param shape mismatch for ID 0x{msg_id:02x} ({entry.name}): "
|
|
236
255
|
f"expected {entry.param_bytes} bytes, got {len(params_bytes)}"
|
|
237
256
|
)
|
|
238
257
|
return None
|
|
239
258
|
|
|
240
|
-
# Decode each param per the catalog grammar.
|
|
259
|
+
# Decode each param per the catalog grammar. A legacy short form stops
|
|
260
|
+
# at the end of its shorter param region.
|
|
241
261
|
values: List[Any] = [] # noqa: UP006
|
|
242
262
|
cursor = 0
|
|
243
263
|
try:
|
|
244
264
|
for ptype, _prender in entry.params:
|
|
265
|
+
if is_legacy and cursor >= len(params_bytes):
|
|
266
|
+
break
|
|
245
267
|
value, cursor = _decode_param(ptype, params_bytes, cursor)
|
|
246
268
|
values.append(value)
|
|
247
269
|
except (IndexError, struct.error, ValueError) as exc:
|
|
@@ -215,3 +215,15 @@ REVISION_2_1 = 0x03 # via EEPROM override only — ADC cannot distinguish
|
|
|
215
215
|
REVISION_2_2 = 0x04 # via EEPROM override only — ADC cannot distinguish
|
|
216
216
|
REVISION_2_3 = 0x05 # R41=10k physical detect
|
|
217
217
|
REVISION_UNKNOWN = 0xFE # ADC band-gap or pre-detect-resistor + A2 indeterminate
|
|
218
|
+
|
|
219
|
+
# Accepted bands for the stored voltage-divider resistors and the calibrated
|
|
220
|
+
# bandgap. Firmware sync: rurp_shield.h VALUE_R1_MIN/MAX and
|
|
221
|
+
# VALUE_R2_MIN/MAX (+/-25 % of 270000 and 44000 ohm), and rurp_voltage_math.h
|
|
222
|
+
# RURP_BANDGAP_MIN_MV/MAX_MV. The firmware refuses a config command with a
|
|
223
|
+
# value outside these bands; the host refuses it first.
|
|
224
|
+
CONFIG_R1_MIN = 202500
|
|
225
|
+
CONFIG_R1_MAX = 337500
|
|
226
|
+
CONFIG_R2_MIN = 33000
|
|
227
|
+
CONFIG_R2_MAX = 55000
|
|
228
|
+
CONFIG_BANDGAP_MIN_MV = 1000
|
|
229
|
+
CONFIG_BANDGAP_MAX_MV = 1200
|
|
@@ -23,7 +23,6 @@ human-input field gates it.
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
-
import datetime
|
|
27
26
|
import hashlib
|
|
28
27
|
import json
|
|
29
28
|
from dataclasses import dataclass, field
|
|
@@ -45,6 +44,7 @@ from firestarter.chip_test import (
|
|
|
45
44
|
repeat_policy_tag,
|
|
46
45
|
resolve_error_name,
|
|
47
46
|
)
|
|
47
|
+
from firestarter.timestamps import iso_cet
|
|
48
48
|
|
|
49
49
|
# ---------------------------------------------------------------------------
|
|
50
50
|
# Module constants -- single sources of truth
|
|
@@ -774,8 +774,8 @@ class DiagnosticReport:
|
|
|
774
774
|
over dataclass fields, so this field's absence from that list is the
|
|
775
775
|
whole exclusion mechanism."""
|
|
776
776
|
|
|
777
|
-
def
|
|
778
|
-
return
|
|
777
|
+
def _now(self) -> str:
|
|
778
|
+
return iso_cet()
|
|
779
779
|
|
|
780
780
|
def _auto_capture_dict(self) -> dict[str, Any]:
|
|
781
781
|
ac = self.auto_capture
|
|
@@ -1065,7 +1065,7 @@ class DiagnosticReport:
|
|
|
1065
1065
|
"""
|
|
1066
1066
|
return {
|
|
1067
1067
|
"schema_version": SCHEMA_VERSION,
|
|
1068
|
-
"generated": self.
|
|
1068
|
+
"generated": self._now(),
|
|
1069
1069
|
"elapsed": self.elapsed,
|
|
1070
1070
|
"auto_capture": self._auto_capture_dict(),
|
|
1071
1071
|
"transport_health": self._transport_dict(),
|
|
@@ -15,7 +15,6 @@ import shutil
|
|
|
15
15
|
import statistics
|
|
16
16
|
import time
|
|
17
17
|
from contextlib import contextmanager
|
|
18
|
-
from datetime import datetime
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
from typing import Callable, Dict, Tuple # noqa: UP035
|
|
21
20
|
|
|
@@ -75,6 +74,7 @@ from firestarter.serial_comm import (
|
|
|
75
74
|
WRITE_BUDGET_MAX_S,
|
|
76
75
|
SerialCommunicator,
|
|
77
76
|
)
|
|
77
|
+
from firestarter.timestamps import iso_cet_basic
|
|
78
78
|
from firestarter.utils import extract_hex_to_decimal
|
|
79
79
|
from firestarter.write_blank_guard import (
|
|
80
80
|
incomplete_refusal_text,
|
|
@@ -551,6 +551,12 @@ class EpromOperator:
|
|
|
551
551
|
# `--full` run, which never sets an abort_predicate, can never be
|
|
552
552
|
# mistaken for this host's own doing.
|
|
553
553
|
self._read_abort_intended: bool = False
|
|
554
|
+
# Whether the ERROR that ended the last `_run_state_machine` call was
|
|
555
|
+
# this host's own deliberate read stop (see `_is_own_read_abort`).
|
|
556
|
+
# Decided ONCE, where the error is caught, so the log level there and
|
|
557
|
+
# `_drive_region_compare`'s verdict can never disagree. Reset per
|
|
558
|
+
# operation alongside `_read_abort_stopped_at`.
|
|
559
|
+
self._read_abort_accepted: bool = False
|
|
554
560
|
# SESS-01 (Phase 206): whether this operator is currently holding one
|
|
555
561
|
# validated link open across multiple calls instead of connecting
|
|
556
562
|
# and tearing down per call. Default off -- joins the transient
|
|
@@ -716,7 +722,11 @@ class EpromOperator:
|
|
|
716
722
|
# A leased setup whose ack was rejected (not a raised
|
|
717
723
|
# error) is a failed setup, treated identically to a cold
|
|
718
724
|
# connect failure: the caller's existing not-`command_dict`
|
|
719
|
-
# guard in `_operation_context` handles it unchanged.
|
|
725
|
+
# guard in `_operation_context` handles it unchanged. The
|
|
726
|
+
# firmware may still be mid-reply, so the held link is not
|
|
727
|
+
# trusted again: drop it, as the SerialError arm above does,
|
|
728
|
+
# and the next operation cold-connects.
|
|
729
|
+
self._disconnect_programmer()
|
|
720
730
|
return None, 0
|
|
721
731
|
buffer_size = self._calculate_buffer_size()
|
|
722
732
|
logger.debug(
|
|
@@ -871,6 +881,7 @@ class EpromOperator:
|
|
|
871
881
|
self.last_firmware_error_code = None
|
|
872
882
|
self.last_firmware_error_message = None
|
|
873
883
|
self._read_abort_stopped_at = None
|
|
884
|
+
self._read_abort_accepted = False
|
|
874
885
|
try:
|
|
875
886
|
with logging_redirect_tqdm():
|
|
876
887
|
# --- INIT Phase ---
|
|
@@ -897,7 +908,14 @@ class EpromOperator:
|
|
|
897
908
|
logger.error(f"Communication error during {operation_name}: {e}")
|
|
898
909
|
return False, str(e)
|
|
899
910
|
except EpromOperationError as e:
|
|
900
|
-
|
|
911
|
+
self._read_abort_accepted = self._is_own_read_abort(e.error_code)
|
|
912
|
+
if self._read_abort_accepted:
|
|
913
|
+
# The host stopped this read on purpose; the caller reports
|
|
914
|
+
# the compare result. An ERROR line here would read as a
|
|
915
|
+
# hardware fault on every non-blank or mismatching chip.
|
|
916
|
+
logger.debug(f"Read stopped by the host during {operation_name}: {e}")
|
|
917
|
+
else:
|
|
918
|
+
logger.error(f"Programmer error during {operation_name}: {e}")
|
|
901
919
|
# Debug session w27c512-devtest-all-bad: record the firmware's
|
|
902
920
|
# own id and text before collapsing this to a bool, so the
|
|
903
921
|
# diagnostic report can state WHY a step failed instead of
|
|
@@ -907,6 +925,35 @@ class EpromOperator:
|
|
|
907
925
|
return False, str(e)
|
|
908
926
|
finally:
|
|
909
927
|
progress.close()
|
|
928
|
+
# A leased link outlives this call: never carry a read stop's
|
|
929
|
+
# expected-error mark into the next operation.
|
|
930
|
+
if self.comm is not None:
|
|
931
|
+
self.comm.expected_error_id = None
|
|
932
|
+
|
|
933
|
+
def _is_own_read_abort(self, error_code: int | None) -> bool:
|
|
934
|
+
"""Whether an ERROR is the consequence of this host's own read stop.
|
|
935
|
+
|
|
936
|
+
D-08: the deliberate stop and a genuine timeout both surface as an
|
|
937
|
+
ERROR carrying MSG_ERR_TIMEOUT -- the two are wire-identical. Accept
|
|
938
|
+
the error as this host's own doing ONLY when all four hold: the
|
|
939
|
+
default path actually asked for a stop (`_read_abort_intended`), the
|
|
940
|
+
read loop actually recorded one (`_read_abort_stopped_at` is not
|
|
941
|
+
`None`), the firmware's own error id is exactly the timeout id (not
|
|
942
|
+
some other fault wearing its clothes), and the stop happened
|
|
943
|
+
recently enough that this error frame could plausibly be its
|
|
944
|
+
consequence (the bounded window). Any single condition failing
|
|
945
|
+
means a real fault -- a mismatching chip whose read failed for an
|
|
946
|
+
unrelated reason must never be reported as a mismatch, and this
|
|
947
|
+
host's own deliberate abort must never be reported as hardware
|
|
948
|
+
trouble.
|
|
949
|
+
"""
|
|
950
|
+
stopped_at = self._read_abort_stopped_at
|
|
951
|
+
return (
|
|
952
|
+
self._read_abort_intended
|
|
953
|
+
and stopped_at is not None
|
|
954
|
+
and error_code == MSG_ERR_TIMEOUT
|
|
955
|
+
and (time.monotonic() - stopped_at) <= READ_ABORT_ACCEPTANCE_WINDOW_S
|
|
956
|
+
)
|
|
910
957
|
|
|
911
958
|
def _execute_phase(
|
|
912
959
|
self, phase_name: str, progress: ClassProgressHandler
|
|
@@ -1210,7 +1257,11 @@ class EpromOperator:
|
|
|
1210
1257
|
if abort_predicate is not None and abort_predicate():
|
|
1211
1258
|
stopped = True
|
|
1212
1259
|
self._read_abort_stopped_at = time.monotonic()
|
|
1213
|
-
|
|
1260
|
+
# The firmware's ack-wait timeout is now the expected
|
|
1261
|
+
# end of this read: log it at DEBUG, not ERROR.
|
|
1262
|
+
# `_run_state_machine` clears this on the way out.
|
|
1263
|
+
self.comm.expected_error_id = MSG_ERR_TIMEOUT
|
|
1264
|
+
logger.debug(
|
|
1214
1265
|
f"Read stopped in flight at 0x{start_addr:06x} "
|
|
1215
1266
|
"(abort predicate fired)."
|
|
1216
1267
|
)
|
|
@@ -1332,7 +1383,7 @@ class EpromOperator:
|
|
|
1332
1383
|
# path is to use the actual board name -- but the unit-test
|
|
1333
1384
|
# surface doesn't have a serial connection, so we fall back).
|
|
1334
1385
|
if output_dir is None:
|
|
1335
|
-
timestamp =
|
|
1386
|
+
timestamp = iso_cet_basic()
|
|
1336
1387
|
output_dir = str(
|
|
1337
1388
|
Path(DEFAULT_RUN_OUTPUT_DIR)
|
|
1338
1389
|
/ f"consistency-check-{eprom_name}-unknown-board-{timestamp}"
|
|
@@ -1509,7 +1560,7 @@ class EpromOperator:
|
|
|
1509
1560
|
source_sha = hashlib.sha256(Path(source_image_path).read_bytes()).hexdigest()
|
|
1510
1561
|
|
|
1511
1562
|
if output_dir is None:
|
|
1512
|
-
timestamp =
|
|
1563
|
+
timestamp = iso_cet_basic()
|
|
1513
1564
|
output_dir = str(
|
|
1514
1565
|
Path(DEFAULT_RUN_OUTPUT_DIR)
|
|
1515
1566
|
/ f"write-cycle-{eprom_name}-unknown-board-{timestamp}"
|
|
@@ -1627,7 +1678,7 @@ class EpromOperator:
|
|
|
1627
1678
|
from firestarter.serial_comm import FaultInjectingSerialCommunicator
|
|
1628
1679
|
|
|
1629
1680
|
if output_dir is None:
|
|
1630
|
-
timestamp =
|
|
1681
|
+
timestamp = iso_cet_basic()
|
|
1631
1682
|
output_dir = f"fault-inject-{eprom_name}-{direction}-{timestamp}"
|
|
1632
1683
|
output_path = Path(output_dir)
|
|
1633
1684
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -1894,7 +1945,7 @@ class EpromOperator:
|
|
|
1894
1945
|
return False
|
|
1895
1946
|
|
|
1896
1947
|
if output_dir is None:
|
|
1897
|
-
timestamp =
|
|
1948
|
+
timestamp = iso_cet_basic()
|
|
1898
1949
|
output_dir = f"fault-inject-latency-{fault_form}-{timestamp}"
|
|
1899
1950
|
output_path = Path(output_dir)
|
|
1900
1951
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -2098,7 +2149,7 @@ class EpromOperator:
|
|
|
2098
2149
|
return False
|
|
2099
2150
|
|
|
2100
2151
|
if output_dir is None:
|
|
2101
|
-
timestamp =
|
|
2152
|
+
timestamp = iso_cet_basic()
|
|
2102
2153
|
output_dir = f"connect-cost-{timestamp}"
|
|
2103
2154
|
output_path = Path(output_dir)
|
|
2104
2155
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -2803,28 +2854,11 @@ class EpromOperator:
|
|
|
2803
2854
|
|
|
2804
2855
|
aborted = False
|
|
2805
2856
|
if not is_ok:
|
|
2806
|
-
# D-08:
|
|
2807
|
-
#
|
|
2808
|
-
#
|
|
2809
|
-
#
|
|
2810
|
-
|
|
2811
|
-
# (`_read_abort_intended`), the read loop actually recorded one
|
|
2812
|
-
# (`stopped_at` is not `None`), the firmware's own error id is
|
|
2813
|
-
# exactly the timeout id (not some other fault wearing its
|
|
2814
|
-
# clothes), and the stop happened recently enough that this
|
|
2815
|
-
# error frame could plausibly be its consequence (the bounded
|
|
2816
|
-
# window). Any single condition failing means a real fault: take
|
|
2817
|
-
# the exit-2 path exactly as before -- a mismatching chip whose
|
|
2818
|
-
# read failed for an unrelated reason must never be reported as
|
|
2819
|
-
# a mismatch, and this host's own deliberate abort must never be
|
|
2820
|
-
# reported as hardware trouble.
|
|
2821
|
-
stopped_at = self._read_abort_stopped_at
|
|
2822
|
-
aborted = (
|
|
2823
|
-
self._read_abort_intended
|
|
2824
|
-
and stopped_at is not None
|
|
2825
|
-
and self.last_firmware_error_code == MSG_ERR_TIMEOUT
|
|
2826
|
-
and (time.monotonic() - stopped_at) <= READ_ABORT_ACCEPTANCE_WINDOW_S
|
|
2827
|
-
)
|
|
2857
|
+
# D-08: `_run_state_machine` already decided, with
|
|
2858
|
+
# `_is_own_read_abort`, whether the error that ended the read was
|
|
2859
|
+
# this host's own stop. Anything else is a real fault: take the
|
|
2860
|
+
# exit-2 path exactly as before.
|
|
2861
|
+
aborted = self._read_abort_accepted
|
|
2828
2862
|
if not aborted:
|
|
2829
2863
|
return 2
|
|
2830
2864
|
|
|
@@ -41,6 +41,7 @@ from firestarter.exceptions import (
|
|
|
41
41
|
)
|
|
42
42
|
from firestarter.fw_release_gate import require_installable_release
|
|
43
43
|
from firestarter.serial_comm import SerialCommunicator
|
|
44
|
+
from firestarter.timestamps import iso_text_to_cet
|
|
44
45
|
|
|
45
46
|
logger = logging.getLogger("Firmware")
|
|
46
47
|
|
|
@@ -62,7 +63,7 @@ class ReleaseInfo(TypedDict):
|
|
|
62
63
|
version: str # tag_name from GitHub API
|
|
63
64
|
tag: str # raw tag_name (same as version for firmware releases)
|
|
64
65
|
channel: str # "stable" or "prerelease"
|
|
65
|
-
published: str # ISO
|
|
66
|
+
published: str # published_at as ISO 8601 in CET
|
|
66
67
|
asset_url: str # browser_download_url for the board-matching .hex asset
|
|
67
68
|
|
|
68
69
|
|
|
@@ -468,7 +469,7 @@ class FirmwareManager:
|
|
|
468
469
|
version=tag,
|
|
469
470
|
tag=tag,
|
|
470
471
|
channel="prerelease" if is_pre else "stable",
|
|
471
|
-
published=r.get("published_at") or "",
|
|
472
|
+
published=iso_text_to_cet(r.get("published_at") or ""),
|
|
472
473
|
asset_url=asset_url,
|
|
473
474
|
)
|
|
474
475
|
)
|