firestarter 3.2.1b1__tar.gz → 3.2.2__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.
Files changed (220) hide show
  1. {firestarter-3.2.1b1/firestarter.egg-info → firestarter-3.2.2}/PKG-INFO +2 -1
  2. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/_version.py +3 -3
  3. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/chip_resolver.py +44 -1
  4. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/cli_handlers.py +41 -11
  5. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/database.py +140 -33
  6. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/diagnostic_report.py +4 -4
  7. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/eprom_info.py +35 -7
  8. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/eprom_operations.py +6 -6
  9. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/exceptions.py +6 -0
  10. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/firmware.py +3 -2
  11. firestarter-3.2.2/firestarter/timestamps.py +83 -0
  12. {firestarter-3.2.1b1 → firestarter-3.2.2/firestarter.egg-info}/PKG-INFO +2 -1
  13. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/SOURCES.txt +4 -9
  14. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/requires.txt +1 -0
  15. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/scm_file_list.json +4 -9
  16. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/scm_version.json +2 -2
  17. {firestarter-3.2.1b1 → firestarter-3.2.2}/pyproject.toml +1 -0
  18. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/__snapshots__/test_characterization.ambr +29 -28
  19. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/conftest.py +35 -0
  20. firestarter-3.2.1b1/tests/golden/wire_dict_baseline.json → firestarter-3.2.2/tests/golden/wire_dict.json +189 -137
  21. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/plan_corpus.py +11 -1
  22. firestarter-3.2.2/tests/test_chip_name_resolution.py +199 -0
  23. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_resolver.py +2 -1
  24. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_derive_plan_no_drop_sweep.py +14 -22
  25. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_derive_plan_structural_sentinel.py +20 -34
  26. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_devtest_firmware_error_propagation.py +11 -4
  27. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_erase_flag_invariants.py +20 -30
  28. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_firmware_install.py +4 -2
  29. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_hardware.py +10 -2
  30. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_page_size_invariants.py +22 -44
  31. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_probe_spurious_setup_ack.py +3 -0
  32. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_protocol_not_implemented_production_path.py +4 -1
  33. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_sdp_db_invariant.py +22 -62
  34. firestarter-3.2.2/tests/test_timestamps.py +87 -0
  35. firestarter-3.2.2/tests/test_wire_dict_equivalence.py +177 -0
  36. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_blank_guard_pinning.py +10 -6
  37. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_149.json +0 -65
  38. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_153.json +0 -265
  39. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_182.json +0 -219
  40. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_194.json +0 -86
  41. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_197.json +0 -20
  42. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_198.json +0 -17
  43. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_199.json +0 -14
  44. firestarter-3.2.1b1/tests/golden/wire_dict_expected_deltas_dip24_2816.json +0 -91
  45. firestarter-3.2.1b1/tests/test_wire_dict_equivalence.py +0 -821
  46. {firestarter-3.2.1b1 → firestarter-3.2.2}/LICENSE +0 -0
  47. {firestarter-3.2.1b1 → firestarter-3.2.2}/MANIFEST.in +0 -0
  48. {firestarter-3.2.1b1 → firestarter-3.2.2}/README.md +0 -0
  49. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/__init__.py +0 -0
  50. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/address_parser.py +0 -0
  51. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/avr_tool.py +0 -0
  52. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/channel.py +0 -0
  53. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/chip_test.py +0 -0
  54. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/codec.py +0 -0
  55. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/compare.py +0 -0
  56. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/config.py +0 -0
  57. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/constants.py +0 -0
  58. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/data/chip_database.json +0 -0
  59. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/data/pinouts.json +0 -0
  60. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/dip24_2816_erase_gate.py +0 -0
  61. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/erase_support.py +0 -0
  62. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/flash4_erase_gate.py +0 -0
  63. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/frame_parser.py +0 -0
  64. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/fw_release_gate.py +0 -0
  65. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/hardware.py +0 -0
  66. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/ic_layout.py +0 -0
  67. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/jp5_gate.py +0 -0
  68. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/jumper_table.py +0 -0
  69. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/lock_status.py +0 -0
  70. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/log_capture.py +0 -0
  71. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/logging_utils.py +0 -0
  72. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/main.py +0 -0
  73. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/messages.py +0 -0
  74. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/page_size_gate.py +0 -0
  75. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/protection_readability.py +0 -0
  76. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/py32_dfu.py +0 -0
  77. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/sdp_capability.py +0 -0
  78. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/sdp_honesty.py +0 -0
  79. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/serial_comm.py +0 -0
  80. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/submit.py +0 -0
  81. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/transport_counters.py +0 -0
  82. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/utils.py +0 -0
  83. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/vpp_display.py +0 -0
  84. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter/write_blank_guard.py +0 -0
  85. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/dependency_links.txt +0 -0
  86. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/entry_points.txt +0 -0
  87. {firestarter-3.2.1b1 → firestarter-3.2.2}/firestarter.egg-info/top_level.txt +0 -0
  88. {firestarter-3.2.1b1 → firestarter-3.2.2}/setup.cfg +0 -0
  89. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/__init__.py +0 -0
  90. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fake_chip.py +0 -0
  91. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/part_number_delta.json +0 -0
  92. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/report_shapes.py +0 -0
  93. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/at28c256-full-all-ok-sdp.json +0 -0
  94. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/attr01-status-axis-transport-fault.json +0 -0
  95. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/gh20-at28c256-fail.json +0 -0
  96. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/gh23-w27e257-fail.json +0 -0
  97. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/gh28-m27c512-fail.json +0 -0
  98. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/gh47-sst27sf512-pass.json +0 -0
  99. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-all-ok.json +0 -0
  100. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-blank-check-bad.json +0 -0
  101. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-canonical-name.json +0 -0
  102. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-comma-joined-name.json +0 -0
  103. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/m27c512-full-runs-1.json +0 -0
  104. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/prune03-synthesized-fingerprint-match.json +0 -0
  105. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/sst27sf512-full-all-ok.json +0 -0
  106. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/sst27sf512-six-step-readback-gated.json +0 -0
  107. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/sst27sf512-six-step.json +0 -0
  108. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/synthetic-arm4-empty-results.json +0 -0
  109. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/synthetic-arm4-no-ok.json +0 -0
  110. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/uv-slot-write-pass.json +0 -0
  111. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/reports/w27e257-full-all-ok.json +0 -0
  112. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/shape_ids.json +0 -0
  113. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/fixtures/synthetic_nonzero_chip_id.py +0 -0
  114. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/golden/build_db_part_specific_constants.json +0 -0
  115. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/golden/chip_database_field_inventory.json +0 -0
  116. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_address_parser.py +0 -0
  117. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_b15_page_size_corroboration.py +0 -0
  118. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_blast_radius_invariance.py +0 -0
  119. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_boot_block_hint.py +0 -0
  120. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_budget_failure_render.py +0 -0
  121. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_bug_characterization.py +0 -0
  122. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_build_db_constant_census.py +0 -0
  123. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_build_db_inclusion.py +0 -0
  124. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_build_db_interpret_timing.py +0 -0
  125. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_build_db_pinout_fork.py +0 -0
  126. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_canonical_part_number.py +0 -0
  127. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_characterization.py +0 -0
  128. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_database_field_inventory.py +0 -0
  129. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_test.py +0 -0
  130. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_test_blank_check_order.py +0 -0
  131. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_test_cycle.py +0 -0
  132. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_test_sdp_leg.py +0 -0
  133. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_test_timing.py +0 -0
  134. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_chip_test_uv_slot_write.py +0 -0
  135. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_cli_handlers.py +0 -0
  136. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_click_group_gate_hook.py +0 -0
  137. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_cobs.py +0 -0
  138. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_codec.py +0 -0
  139. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_codec_format_message.py +0 -0
  140. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_compare.py +0 -0
  141. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_config.py +0 -0
  142. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_config_band_refusal.py +0 -0
  143. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_connect_cost_harness.py +0 -0
  144. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_consistency_check.py +0 -0
  145. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_coverage_floor_v18.py +0 -0
  146. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_database_conversion.py +0 -0
  147. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_datasheet_overrides.py +0 -0
  148. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_decoder.py +0 -0
  149. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_dev_group_channel_gating.py +0 -0
  150. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_dev_test_cmd.py +0 -0
  151. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_dev_tools_channel_gate.py +0 -0
  152. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_dfu_opcode_anchors.py +0 -0
  153. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_diagnostic_report.py +0 -0
  154. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_dip24_2816_erase_gate.py +0 -0
  155. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_endpoint_constants.py +0 -0
  156. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_eprom_database.py +0 -0
  157. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_eprom_info.py +0 -0
  158. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_eprom_operations.py +0 -0
  159. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_erase_blank_step_nonregression.py +0 -0
  160. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_error_code_seam.py +0 -0
  161. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_even_block.py +0 -0
  162. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_extra_chips_supplement.py +0 -0
  163. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_flash4_erase_gate.py +0 -0
  164. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fw_list_failure_vs_empty.py +0 -0
  165. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fw_port_targeting_and_blind_install.py +0 -0
  166. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fw_release_gate.py +0 -0
  167. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fw_update_dead_endpoint.py +0 -0
  168. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fw_update_path_gate.py +0 -0
  169. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fw_version_guard.py +0 -0
  170. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_fwguard.py +0 -0
  171. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_hw_revision_gate.py +0 -0
  172. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_ic_layout.py +0 -0
  173. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_info_vpp_and_erase.py +0 -0
  174. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_jp5_gate.py +0 -0
  175. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_jumper_table.py +0 -0
  176. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_lock_status_class_partition.py +0 -0
  177. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_lock_status_cli.py +0 -0
  178. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_lock_status_wire.py +0 -0
  179. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_log_capture.py +0 -0
  180. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_logging_utils.py +0 -0
  181. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_matrix_artifact.py +0 -0
  182. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_page_size_alignment_refusal.py +0 -0
  183. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_page_size_write_refusal.py +0 -0
  184. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_parse_devtest_issue.py +0 -0
  185. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_programming_vcc_census.py +0 -0
  186. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_protect_flags_doc_measurements.py +0 -0
  187. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_protection_resolution.py +0 -0
  188. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_protection_status_catalog.py +0 -0
  189. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_protection_table_citations.py +0 -0
  190. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_protocol_not_implemented.py +0 -0
  191. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_provenance.py +0 -0
  192. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_pulse_us_override.py +0 -0
  193. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_py32_channel_gating.py +0 -0
  194. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_py32_dfu.py +0 -0
  195. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_py32_flash_map_host.py +0 -0
  196. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_py32_pyusb_absent.py +0 -0
  197. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_pyusb_api_surface.py +0 -0
  198. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_sdp_capability.py +0 -0
  199. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_sdp_honesty.py +0 -0
  200. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_serial_characterization.py +0 -0
  201. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_serial_comm.py +0 -0
  202. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_session_lease.py +0 -0
  203. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_setup_refusals.py +0 -0
  204. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_submit.py +0 -0
  205. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_transport_counters.py +0 -0
  206. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_utils.py +0 -0
  207. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_uv_mask.py +0 -0
  208. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_validate_family_cmd.py +0 -0
  209. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_validate_oracle.py +0 -0
  210. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_validated_parts_regression_surface.py +0 -0
  211. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_vcc_margin_rail.py +0 -0
  212. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_vpp_decode_table.py +0 -0
  213. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_vpp_rail_classification.py +0 -0
  214. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_blank_guard.py +0 -0
  215. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_guard_port_pinning.py +0 -0
  216. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_progress.py +0 -0
  217. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_response_budget.py +0 -0
  218. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_skip_erase_0x0d.py +0 -0
  219. {firestarter-3.2.1b1 → firestarter-3.2.2}/tests/test_write_skip_sdp_unlock.py +0 -0
  220. {firestarter-3.2.1b1 → firestarter-3.2.2}/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.1b1
3
+ Version: 3.2.2
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.1b1'
22
- __version_tuple__ = version_tuple = (3, 2, 1, 'b1')
21
+ __version__ = version = '3.2.2'
22
+ __version_tuple__ = version_tuple = (3, 2, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g98e2959'
24
+ __commit_id__ = commit_id = 'ga7d562c'
@@ -10,11 +10,50 @@ Single chokepoint between CLI dispatch and the EPROM database lookup/conversion.
10
10
  from typing import Any
11
11
 
12
12
  from firestarter.database import EpromDatabase
13
- from firestarter.exceptions import ChipNotFoundError, ChipNotImplementedError
13
+ from firestarter.exceptions import (
14
+ AmbiguousChipNameError,
15
+ ChipNotFoundError,
16
+ ChipNotImplementedError,
17
+ )
14
18
 
15
19
  _NON_ZERO_WIRE_FIELDS = ("memory-size", "pin-count", "vpp_mv")
16
20
 
17
21
 
22
+ def _qualified_choices(name: str, db: EpromDatabase) -> list[str]:
23
+ """The row `name` selects first, then each other row it selects that
24
+ programs differently, all as qualified names. Empty when one row, or rows
25
+ that program the same, are selected."""
26
+ alternatives = db.ambiguous_alternatives(name)
27
+ if not alternatives:
28
+ return []
29
+ raw_config, manufacturer = db.get_eprom_config(name)
30
+ return [db.qualified_chip_name(name, raw_config, manufacturer), *alternatives]
31
+
32
+
33
+ def refuse_if_ambiguous(name: str, db: EpromDatabase) -> None:
34
+ """Refuse a name that selects chips that program differently: guessing
35
+ one would drive the wrong VPP, algorithm or timing."""
36
+ choices = _qualified_choices(name, db)
37
+ if choices:
38
+ listed = ", ".join(f"'{c}'" for c in choices)
39
+ raise AmbiguousChipNameError(
40
+ f"'{name}' names chips that program differently. Name one: {listed}."
41
+ )
42
+
43
+
44
+ def describe_ambiguity(name: str, db: EpromDatabase) -> str | None:
45
+ """For display commands, which use the first row: the other rows `name`
46
+ selects, or None."""
47
+ choices = _qualified_choices(name, db)
48
+ if not choices:
49
+ return None
50
+ others = ", ".join(f"'{c}'" for c in choices[1:])
51
+ return (
52
+ f"'{name}' also names chips that program differently: {others}. "
53
+ f"This shows '{choices[0]}'."
54
+ )
55
+
56
+
18
57
  def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
19
58
  """Resolve a chip name to its programmer-config dict.
20
59
 
@@ -26,6 +65,9 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
26
65
  conversion (``convert_to_programmer`` returns ``{}``) are treated as the
27
66
  not-found condition.
28
67
 
68
+ Raises ``AmbiguousChipNameError`` (a ``ChipNotFoundError``) when the name
69
+ selects rows that program differently; the message names each of them.
70
+
29
71
  Raises ``ChipNotImplementedError`` when the chip exists in the database but
30
72
  its ``support_status`` is not ``"supported"`` (covers protocol-not-implemented,
31
73
  adapter-required, and vpp-exceeds-max). This guard fires BEFORE any wire dict
@@ -49,6 +91,7 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
49
91
  # have a support_status, so raise ChipNotFoundError immediately.
50
92
  if raw_config is None:
51
93
  raise ChipNotFoundError(name)
94
+ refuse_if_ambiguous(name, db)
52
95
 
53
96
  # Support-status guard: refuse every program-capable operation
54
97
  # for non-supported chips BEFORE convert_to_programmer builds any wire dict.
@@ -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
@@ -39,7 +38,11 @@ from firestarter.channel import (
39
38
  dev_command_gate_message,
40
39
  is_dev_tools_enabled,
41
40
  )
42
- from firestarter.chip_resolver import resolve_chip
41
+ from firestarter.chip_resolver import (
42
+ describe_ambiguity,
43
+ refuse_if_ambiguous,
44
+ resolve_chip,
45
+ )
43
46
  from firestarter.chip_test import (
44
47
  OP_ID,
45
48
  SDP_HOLD_NOT_RUN,
@@ -106,6 +109,7 @@ from firestarter.protection_readability import (
106
109
  protection_gate_for_entry,
107
110
  )
108
111
  from firestarter.sdp_capability import SDP_PROTOCOL_ID, sdp_capability
112
+ from firestarter.timestamps import iso_cet
109
113
 
110
114
  logger = logging.getLogger("Firestarter")
111
115
 
@@ -160,10 +164,17 @@ def _complete_eprom(
160
164
  semantics at main.py:39-45.
161
165
  """ # noqa: E501
162
166
  db = EpromDatabase()
163
- return [
164
- click.shell_completion.CompletionItem(e["name"])
167
+ qualified = db.names_needing_qualifier()
168
+ # A row whose name also names chips that program differently is offered
169
+ # by the qualified name that selects it.
170
+ names = (
171
+ qualified.get((e["manufacturer"], e["name"]), e["name"])
165
172
  for e in db.get_eproms()
166
- if e["name"].lower().startswith(incomplete.lower())
173
+ )
174
+ return [
175
+ click.shell_completion.CompletionItem(name)
176
+ for name in names
177
+ if name.lower().startswith(incomplete.lower())
167
178
  ]
168
179
 
169
180
 
@@ -486,7 +497,11 @@ def _list_cmd(app: AppContext) -> None:
486
497
  """List all EPROMs in the database."""
487
498
  eprom_data_list = app.db.get_eproms()
488
499
  if eprom_data_list:
489
- print_eprom_list_table(eprom_data_list, app.eprom_presenter.spec_builder)
500
+ print_eprom_list_table(
501
+ eprom_data_list,
502
+ app.eprom_presenter.spec_builder,
503
+ app.db.names_needing_qualifier(),
504
+ )
490
505
  sys.exit(0)
491
506
  sys.exit(1)
492
507
 
@@ -504,6 +519,9 @@ def info(app: AppContext, eprom: str, config: bool, adapter: bool) -> None:
504
519
  logger.error(f"EPROM '{eprom}' not found in database.")
505
520
  sys.exit(1)
506
521
 
522
+ ambiguity = describe_ambiguity(eprom, app.db)
523
+ if ambiguity:
524
+ logger.warning(ambiguity)
507
525
  eprom_data_for_programmer = app.db.convert_to_programmer(eprom_details)
508
526
  raw_config_data, manufacturer = app.db.get_eprom_config(eprom)
509
527
 
@@ -534,7 +552,11 @@ def search(app: AppContext, text: str) -> None:
534
552
  """Search for EPROMs in the database."""
535
553
  search_results = app.db.search_eprom(text)
536
554
  if search_results:
537
- print_eprom_list_table(search_results, app.eprom_presenter.spec_builder)
555
+ print_eprom_list_table(
556
+ search_results,
557
+ app.eprom_presenter.spec_builder,
558
+ app.db.names_needing_qualifier(),
559
+ )
538
560
  sys.exit(0)
539
561
  sys.exit(1)
540
562
 
@@ -1505,7 +1527,9 @@ def chip_id(app: AppContext, eprom: str, force: bool) -> None:
1505
1527
  for ic in found_eproms_for_detected_id
1506
1528
  ]
1507
1529
  print_eprom_list_table(
1508
- mapped_found_eproms, app.eprom_presenter.spec_builder
1530
+ mapped_found_eproms,
1531
+ app.eprom_presenter.spec_builder,
1532
+ app.db.names_needing_qualifier(),
1509
1533
  )
1510
1534
  else:
1511
1535
  logger.warning(
@@ -1908,10 +1932,10 @@ def fw(
1908
1932
 
1909
1933
  print(_json.dumps(releases, indent=2))
1910
1934
  else:
1911
- print(f"{'Version':<12} {'Channel':<14} {'Published':<22} Asset URL")
1935
+ print(f"{'Version':<12} {'Channel':<14} {'Published':<25} Asset URL")
1912
1936
  for r in releases:
1913
1937
  print(
1914
- f"{r['version']:<12} {r['channel']:<14} {r['published']:<22} {r['asset_url']}" # noqa: E501
1938
+ f"{r['version']:<12} {r['channel']:<14} {r['published']:<25} {r['asset_url']}" # noqa: E501
1915
1939
  )
1916
1940
  if not releases:
1917
1941
  print(f"No releases found for board {board}.")
@@ -2638,7 +2662,7 @@ def _write_artifact(
2638
2662
  out_path.mkdir(parents=True, exist_ok=True)
2639
2663
 
2640
2664
  artifact: Dict[str, Any] = { # noqa: UP006
2641
- "generated": datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
2665
+ "generated": iso_cet(),
2642
2666
  "harness_version": "71",
2643
2667
  "cells": cells,
2644
2668
  }
@@ -3058,6 +3082,8 @@ def _canonical_part_number(part_number: str | None, raw_token: str) -> str | Non
3058
3082
  def _strip_paren(s: str) -> str:
3059
3083
  return re.sub(r"\([^)]*\)", "", s).strip().lower()
3060
3084
 
3085
+ # A manufacturer qualifier (PART@MANUFACTURER) is not part of the name.
3086
+ raw_token = raw_token.partition("@")[0].strip()
3061
3087
  query = raw_token.lower()
3062
3088
  if query == part_number.lower():
3063
3089
  return part_number
@@ -3166,6 +3192,10 @@ def dev_test(app: "AppContext", chip: str, fast: bool, submit: bool) -> None:
3166
3192
  # still runs the full community-validation sweep below.
3167
3193
  if not app.db.get_eprom(chip):
3168
3194
  raise ChipNotFoundError(f"{chip}: not found in database")
3195
+ # A name that selects chips that program differently is refused here,
3196
+ # not step by step: every step would be SKIPPED and the report would
3197
+ # name no chip.
3198
+ refuse_if_ambiguous(chip, app.db)
3169
3199
 
3170
3200
  # The chip must be known to be in the DB (see above) before its
3171
3201
  # electrical type can be read, so the UV-scope resolution happens here,
@@ -14,6 +14,7 @@ return the same instance.
14
14
  import json
15
15
  import logging
16
16
  import os
17
+ import re
17
18
  from pathlib import Path
18
19
  from typing import Any
19
20
 
@@ -145,6 +146,11 @@ def _read_config_file(filename: str) -> dict:
145
146
  return {}
146
147
 
147
148
 
149
+ def _strip_mode(name: str) -> str:
150
+ """'DS1245AB(RW)' -> 'ds1245ab': the name without its mode annotation."""
151
+ return re.sub(r"\([^)]*\)", "", name).strip().lower()
152
+
153
+
148
154
  class EpromDatabase:
149
155
  """
150
156
  Manages the EPROM and pin map database for the Firestarter application.
@@ -157,6 +163,7 @@ class EpromDatabase:
157
163
  def __init__(self, skip_local_override: bool = False):
158
164
  self.proms: dict[str, Any] = {}
159
165
  self.pin_maps: dict[str, Any] = {}
166
+ self._names_needing_qualifier: dict[tuple[str, str], str] | None = None
160
167
  self._initialize_database_core(skip_local_override=skip_local_override)
161
168
  logger.debug("EpromDatabase initialized.")
162
169
 
@@ -426,46 +433,146 @@ class EpromDatabase:
426
433
  for ic_config in ics
427
434
  ]
428
435
 
436
+ def find_chip_configs(self, chip_name: str) -> list[tuple[dict, str]]:
437
+ """Return every database row `chip_name` selects, as
438
+ (config_dict, manufacturer) pairs in database order.
439
+
440
+ A name matches a row's `part_number` directly, or one of its
441
+ comma-separated aliases. Upstream infoic.xml encodes chip aliases as
442
+ comma-separated lists (e.g. "AT28C256,AT28C256E,AT28HC256"), which
443
+ build_db.py preserves in the part_number field.
444
+
445
+ infoic.xml also annotates many names with a parenthetical mode, such
446
+ as "(RW)", "(TEST)" or "(RW3.3V)" (e.g. "DS1245AB(RW),DS1245Y(RW)"). A
447
+ name matches an annotated alias with the annotation stripped, but only
448
+ when no row matches the name as typed: "DS1220(TEST)" selects the
449
+ (TEST) row, never the (RW) row that "DS1220" also matches.
450
+
451
+ The same part number can name rows of different manufacturers, which
452
+ program differently. `PART@MANUFACTURER` selects one manufacturer's
453
+ rows; see `resolve_manufacturer` for how the manufacturer matches.
454
+ """
455
+ name, at, wanted = chip_name.partition("@")
456
+ manufacturer_wanted = self.resolve_manufacturer(wanted) if at else None
457
+ if at and manufacturer_wanted is None:
458
+ return []
459
+ query = name.strip().lower()
460
+ query_stripped = _strip_mode(name)
461
+ exact: list[tuple[dict, str]] = []
462
+ stripped: list[tuple[dict, str]] = []
463
+ for manufacturer, ics in self.proms.items():
464
+ if manufacturer_wanted is not None and manufacturer != manufacturer_wanted:
465
+ continue
466
+ for ic_config in ics:
467
+ part_number = ic_config.get("part_number", "")
468
+ aliases = part_number.split(",")
469
+ if query == part_number.lower() or query in (
470
+ a.strip().lower() for a in aliases
471
+ ):
472
+ exact.append((ic_config, manufacturer))
473
+ elif query_stripped and query_stripped in (
474
+ _strip_mode(a) for a in aliases
475
+ ):
476
+ stripped.append((ic_config, manufacturer))
477
+ return exact or stripped
478
+
429
479
  def get_eprom_config(self, chip_name: str):
430
480
  """
431
481
  Retrieves the raw configuration data for a specific EPROM by its name.
432
482
  Returns (config_dict, manufacturer_str) or (None, None).
433
483
 
434
- Matches against `part_number` directly OR against any of the
435
- comma-separated alias names within it. Upstream infoic.xml encodes
436
- chip aliases as comma-separated lists (e.g.,
437
- "AT28C256,AT28C256E,AT28HC256"), which build_db.py preserves in
438
- the part_number field. Without alias-aware lookup, queries like
439
- `firestarter info AT28C256` failed despite the chip being in the DB.
440
-
441
- Also handles infoic.xml's parenthetical mode annotations: many
442
- chip names carry "(RW)", "(TEST)", "(RW3.3V)" etc. (e.g.,
443
- "DS1245AB(RW),DS1245Y(RW)"). A plain query like "DS1245AB"
444
- is normalized to match against the paren-stripped alias.
484
+ The first row `find_chip_configs` selects; see there for how a name
485
+ matches.
445
486
  """
446
- import re
447
-
448
- def _strip_paren(s):
449
- # "DS1245AB(RW)" -> "DS1245AB"; preserves the canonical chip name.
450
- return re.sub(r"\([^)]*\)", "", s).strip().lower()
487
+ matches = self.find_chip_configs(chip_name)
488
+ return matches[0] if matches else (None, None)
489
+
490
+ def resolve_manufacturer(self, wanted: str) -> str | None:
491
+ """The manufacturer `wanted` names: an exact name, ignoring case, else
492
+ the one manufacturer it is a prefix of. None when it names none, or is
493
+ a prefix of more than one."""
494
+ wanted = wanted.strip().lower()
495
+ if not wanted:
496
+ return None
497
+ names = list(self.proms)
498
+ exact = [m for m in names if m.lower() == wanted]
499
+ if exact:
500
+ return exact[0]
501
+ prefixed = [m for m in names if m.lower().startswith(wanted)]
502
+ return prefixed[0] if len(prefixed) == 1 else None
503
+
504
+ def qualified_chip_name(
505
+ self, chip_name: str, ic_config: dict, manufacturer: str
506
+ ) -> str:
507
+ """The name that selects this one row: the row's alias that `chip_name`
508
+ matched, with its mode annotation, then `@` and the manufacturer.
509
+
510
+ The manufacturer is its first word when that word names it alone, so
511
+ the name needs no shell quoting ('27C128@MICROCHIP', not
512
+ '27C128@MICROCHIP memory').
513
+ """
514
+ name = chip_name.partition("@")[0].strip()
515
+ aliases = [a.strip() for a in ic_config.get("part_number", "").split(",")]
516
+ alias = next(
517
+ (a for a in aliases if a.lower() == name.lower()),
518
+ next(
519
+ (a for a in aliases if _strip_mode(a) == _strip_mode(name)),
520
+ aliases[0],
521
+ ),
522
+ )
523
+ first_word = manufacturer.split()[0] if manufacturer.split() else manufacturer
524
+ short = (
525
+ first_word
526
+ if self.resolve_manufacturer(first_word) == manufacturer
527
+ else manufacturer
528
+ )
529
+ return f"{alias}@{short}"
530
+
531
+ def names_needing_qualifier(self) -> dict[tuple[str, str], str]:
532
+ """Every row with an alias that, typed as shown, also selects rows that
533
+ program differently, keyed (manufacturer, part_number), with the
534
+ qualified name that selects it. Such a row can only be named with
535
+ `@MANUFACTURER`."""
536
+ if self._names_needing_qualifier is None:
537
+ by_name: dict[str, list[tuple[dict, str]]] = {}
538
+ for manufacturer, ics in self.proms.items():
539
+ for ic_config in ics:
540
+ for alias in ic_config.get("part_number", "").split(","):
541
+ by_name.setdefault(alias.strip().lower(), []).append(
542
+ (ic_config, manufacturer)
543
+ )
544
+ needing: dict[tuple[str, str], str] = {}
545
+ for name, rows in by_name.items():
546
+ if len(rows) < 2:
547
+ continue
548
+ wires = [
549
+ self.convert_to_programmer(self._map_data(ic, m)) for ic, m in rows
550
+ ]
551
+ if all(w == wires[0] for w in wires[1:]):
552
+ continue
553
+ for ic, m in rows:
554
+ needing.setdefault(
555
+ (m, ic.get("part_number", "")),
556
+ self.qualified_chip_name(name, ic, m),
557
+ )
558
+ self._names_needing_qualifier = needing
559
+ return self._names_needing_qualifier
451
560
 
452
- query = chip_name.lower()
453
- query_stripped = _strip_paren(chip_name)
454
- for manufacturer, ics in self.proms.items():
455
- for ic_config in ics:
456
- part_number = ic_config.get("part_number", "")
457
- if query == part_number.lower():
458
- return ic_config, manufacturer
459
- # Alias match: split on comma + match individual aliases.
460
- # Try exact alias first, then paren-stripped alias.
461
- if "," in part_number or "(" in part_number:
462
- aliases = [a.strip().lower() for a in part_number.split(",")]
463
- if query in aliases:
464
- return ic_config, manufacturer
465
- aliases_stripped = [_strip_paren(a) for a in part_number.split(",")]
466
- if query_stripped and query_stripped in aliases_stripped:
467
- return ic_config, manufacturer
468
- return None, None
561
+ def ambiguous_alternatives(self, chip_name: str) -> list[str]:
562
+ """Name the other rows `chip_name` selects that program differently
563
+ from the one `get_eprom_config` uses, each as a qualified name that
564
+ selects that row alone. Empty when the choice cannot matter.
565
+ """
566
+ matches = self.find_chip_configs(chip_name)
567
+ if len(matches) < 2:
568
+ return []
569
+ chosen = self.convert_to_programmer(self._map_data(*matches[0]))
570
+ return [
571
+ self.qualified_chip_name(chip_name, ic_config, manufacturer)
572
+ for ic_config, manufacturer in matches[1:]
573
+ if self.convert_to_programmer(self._map_data(ic_config, manufacturer))
574
+ != chosen
575
+ ]
469
576
 
470
577
  def get_eprom(self, chip_name: str):
471
578
  """
@@ -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 _utc_now(self) -> str:
778
- return datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
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._utc_now(),
1068
+ "generated": self._now(),
1069
1069
  "elapsed": self.elapsed,
1070
1070
  "auto_capture": self._auto_capture_dict(),
1071
1071
  "transport_health": self._transport_dict(),
@@ -394,9 +394,17 @@ class EpromConsolePresenter:
394
394
 
395
395
 
396
396
  # --- Helper function for printing EPROM list (for testing/CLI) ---
397
- def print_eprom_list_table(eproms_data: list, spec_builder: EpromSpecBuilder):
397
+ def print_eprom_list_table(
398
+ eproms_data: list,
399
+ spec_builder: EpromSpecBuilder,
400
+ qualified_names: dict[tuple[str, str], str] | None = None,
401
+ ):
398
402
  """Prints a list of EPROM data in a table format. For CLI/testing.
399
403
 
404
+ A row in `qualified_names` (keyed (manufacturer, name)) can only be named
405
+ with `@MANUFACTURER`; its Name cell ends in '*' and a line under the table
406
+ gives an example.
407
+
400
408
  Column layout:
401
409
  - Name: dynamic width clamped to [13, 20]; names longer than 20 chars are
402
410
  truncated with a trailing ellipsis ('…') that counts toward the 20-char cap.
@@ -413,19 +421,26 @@ def print_eprom_list_table(eproms_data: list, spec_builder: EpromSpecBuilder):
413
421
  # Compute the widest rendered name (including any [!] suffix) across all rows,
414
422
  # then clamp to the [13, 20] range. Names that would exceed 20 chars are
415
423
  # truncated to 19 chars + '…' (ellipsis counts toward the 20-char cap).
416
- def _render_name(raw_name: str, has_bus_config: bool) -> str:
424
+ qualified_names = qualified_names or {}
425
+
426
+ def _render_name(raw_name: str, has_bus_config: bool, marked: bool) -> str:
417
427
  """Return the name string as it will be rendered in the table cell."""
418
428
  name = raw_name
419
429
  if not has_bus_config:
420
430
  name = (name[:11] + "[!]") if len(name) > 11 else f"{name}[!]"
421
- if len(name) > 20:
422
- name = name[:19] + "…" # ellipsis counts toward the 20-char cap
423
- return name
431
+ cap = 19 if marked else 20
432
+ if len(name) > cap:
433
+ name = name[: cap - 1] + "…" # ellipsis counts toward the cap
434
+ return name + "*" if marked else name
424
435
 
425
- rendered_names = [
426
- _render_name(ic.get("name", ""), bool(ic.get("bus-config")))
436
+ marked_rows = [
437
+ (ic.get("manufacturer", ""), ic.get("name", "")) in qualified_names
427
438
  for ic in eproms_data
428
439
  ]
440
+ rendered_names = [
441
+ _render_name(ic.get("name", ""), bool(ic.get("bus-config")), marked)
442
+ for ic, marked in zip(eproms_data, marked_rows)
443
+ ]
429
444
  name_w = max(13, min(20, max((len(n) for n in rendered_names), default=13)))
430
445
 
431
446
  divider = f"+{'':-<{name_w + 1}}+{'':-<18}+{'':-<6}+{'':-<12}+{'':-<13}+{'':-<6}+"
@@ -462,6 +477,19 @@ def print_eprom_list_table(eproms_data: list, spec_builder: EpromSpecBuilder):
462
477
  f"| {name: <{name_w}}| {ic.get('manufacturer', ''): <17}|{ic.get('pin-count', 0): >5} | {chip_id_str: <11}| {type_str_display: <12}| {vpp_str: <5}|" # noqa: E501
463
478
  )
464
479
  logger.info(divider)
480
+ example = next(
481
+ (
482
+ qualified_names[(ic.get("manufacturer", ""), ic.get("name", ""))]
483
+ for ic, marked in zip(eproms_data, marked_rows)
484
+ if marked
485
+ ),
486
+ None,
487
+ )
488
+ if example:
489
+ logger.info(
490
+ "* This name also selects chips that program differently. "
491
+ f"Add @MANUFACTURER to choose one, e.g. {example}"
492
+ )
465
493
 
466
494
 
467
495
  # Standalone test function
@@ -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,
@@ -1383,7 +1383,7 @@ class EpromOperator:
1383
1383
  # path is to use the actual board name -- but the unit-test
1384
1384
  # surface doesn't have a serial connection, so we fall back).
1385
1385
  if output_dir is None:
1386
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
1386
+ timestamp = iso_cet_basic()
1387
1387
  output_dir = str(
1388
1388
  Path(DEFAULT_RUN_OUTPUT_DIR)
1389
1389
  / f"consistency-check-{eprom_name}-unknown-board-{timestamp}"
@@ -1560,7 +1560,7 @@ class EpromOperator:
1560
1560
  source_sha = hashlib.sha256(Path(source_image_path).read_bytes()).hexdigest()
1561
1561
 
1562
1562
  if output_dir is None:
1563
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
1563
+ timestamp = iso_cet_basic()
1564
1564
  output_dir = str(
1565
1565
  Path(DEFAULT_RUN_OUTPUT_DIR)
1566
1566
  / f"write-cycle-{eprom_name}-unknown-board-{timestamp}"
@@ -1678,7 +1678,7 @@ class EpromOperator:
1678
1678
  from firestarter.serial_comm import FaultInjectingSerialCommunicator
1679
1679
 
1680
1680
  if output_dir is None:
1681
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
1681
+ timestamp = iso_cet_basic()
1682
1682
  output_dir = f"fault-inject-{eprom_name}-{direction}-{timestamp}"
1683
1683
  output_path = Path(output_dir)
1684
1684
  output_path.mkdir(parents=True, exist_ok=True)
@@ -1945,7 +1945,7 @@ class EpromOperator:
1945
1945
  return False
1946
1946
 
1947
1947
  if output_dir is None:
1948
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
1948
+ timestamp = iso_cet_basic()
1949
1949
  output_dir = f"fault-inject-latency-{fault_form}-{timestamp}"
1950
1950
  output_path = Path(output_dir)
1951
1951
  output_path.mkdir(parents=True, exist_ok=True)
@@ -2149,7 +2149,7 @@ class EpromOperator:
2149
2149
  return False
2150
2150
 
2151
2151
  if output_dir is None:
2152
- timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
2152
+ timestamp = iso_cet_basic()
2153
2153
  output_dir = f"connect-cost-{timestamp}"
2154
2154
  output_path = Path(output_dir)
2155
2155
  output_path.mkdir(parents=True, exist_ok=True)
@@ -195,3 +195,9 @@ class ChipNotFoundError(Exception):
195
195
  """
196
196
 
197
197
  pass
198
+
199
+
200
+ class AmbiguousChipNameError(ChipNotFoundError):
201
+ """Raised when a chip name selects database rows that program differently
202
+ (VPP, algorithm, chip ID or pulse width), so no one chip is named. The
203
+ message lists a qualified name for each of them."""
@@ -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-8601 from published_at field
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
  )