firestarter 3.2.7b2__tar.gz → 3.3.0b1__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 (236) hide show
  1. {firestarter-3.2.7b2/firestarter.egg-info → firestarter-3.3.0b1}/PKG-INFO +1 -1
  2. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/_version.py +3 -3
  3. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/channel.py +8 -15
  4. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/chip_resolver.py +27 -2
  5. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/cli_handlers.py +74 -39
  6. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/codec.py +73 -33
  7. firestarter-3.3.0b1/firestarter/command_codec.py +188 -0
  8. firestarter-3.3.0b1/firestarter/command_frame.py +105 -0
  9. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/constants.py +21 -28
  10. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/data/chip_database.json +383 -262
  11. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/data/pinouts.json +11 -1
  12. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/database.py +2 -2
  13. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/diagnostic_report.py +1 -1
  14. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/eprom_info.py +2 -1
  15. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/eprom_operations.py +87 -105
  16. firestarter-3.3.0b1/firestarter/eprom_rows.py +12 -0
  17. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/exceptions.py +19 -0
  18. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/firmware.py +93 -14
  19. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/frame_parser.py +7 -2
  20. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/fw_release_gate.py +86 -15
  21. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/hardware.py +120 -85
  22. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/ic_layout.py +3 -1
  23. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/jumper_table.py +10 -0
  24. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/messages.py +102 -10
  25. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/page_size_gate.py +4 -4
  26. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/serial_comm.py +135 -80
  27. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/vpp_display.py +8 -1
  28. firestarter-3.3.0b1/firestarter/write_budget.py +74 -0
  29. {firestarter-3.2.7b2 → firestarter-3.3.0b1/firestarter.egg-info}/PKG-INFO +1 -1
  30. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter.egg-info/SOURCES.txt +14 -2
  31. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter.egg-info/scm_file_list.json +14 -4
  32. firestarter-3.3.0b1/firestarter.egg-info/scm_version.json +8 -0
  33. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/__snapshots__/test_characterization.ambr +2 -2
  34. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/conftest.py +221 -95
  35. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fake_chip.py +4 -33
  36. firestarter-3.3.0b1/tests/fixtures/operator_doubles.py +114 -0
  37. firestarter-3.3.0b1/tests/fixtures/ready_ack.py +46 -0
  38. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/report_shapes.py +24 -126
  39. firestarter-3.3.0b1/tests/fresh_cli.py +116 -0
  40. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/golden/chip_database_field_inventory.json +5 -32
  41. firestarter-3.3.0b1/tests/golden/command_vectors.json +435 -0
  42. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/golden/wire_dict.json +41 -4
  43. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/plan_corpus.py +29 -49
  44. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_b15_page_size_corroboration.py +0 -55
  45. firestarter-3.3.0b1/tests/test_bandgap_back_solve.py +64 -0
  46. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_blast_radius_invariance.py +4 -177
  47. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_boot_block_hint.py +7 -20
  48. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_budget_failure_render.py +32 -89
  49. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_bug_characterization.py +17 -24
  50. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_build_db_inclusion.py +111 -5
  51. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_canonical_part_number.py +5 -46
  52. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_characterization.py +7 -31
  53. firestarter-3.3.0b1/tests/test_chip_database_field_inventory.py +220 -0
  54. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_name_resolution.py +38 -46
  55. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_resolver.py +142 -1
  56. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_test.py +309 -460
  57. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_test_blank_check_order.py +7 -12
  58. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_test_cycle.py +17 -40
  59. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_test_sdp_leg.py +93 -336
  60. firestarter-3.3.0b1/tests/test_chip_test_timing.py +145 -0
  61. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_chip_test_uv_slot_write.py +35 -122
  62. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_cli_handlers.py +72 -238
  63. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_cobs.py +2 -57
  64. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_codec.py +7 -9
  65. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_codec_format_message.py +81 -47
  66. firestarter-3.3.0b1/tests/test_command_codec.py +314 -0
  67. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_compare.py +86 -166
  68. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_config.py +11 -20
  69. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_config_band_refusal.py +96 -19
  70. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_connect_cost_harness.py +1 -7
  71. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_consistency_check.py +153 -210
  72. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_coverage_floor_v18.py +6 -17
  73. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_database_conversion.py +3 -1
  74. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_datasheet_overrides.py +5 -18
  75. firestarter-3.3.0b1/tests/test_decoder.py +475 -0
  76. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_derive_plan_no_drop_sweep.py +3 -109
  77. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_derive_plan_structural_sentinel.py +53 -319
  78. firestarter-3.3.0b1/tests/test_dev_group_channel_gating.py +187 -0
  79. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_dev_test_cmd.py +40 -150
  80. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_dev_tools_channel_gate.py +23 -7
  81. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_devtest_firmware_error_propagation.py +4 -15
  82. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_diagnostic_report.py +213 -384
  83. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_dip24_2816_erase_gate.py +12 -13
  84. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_eprom_info.py +3 -1
  85. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_eprom_operations.py +289 -774
  86. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_erase_blank_step_nonregression.py +4 -27
  87. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_erase_flag_invariants.py +15 -25
  88. firestarter-3.3.0b1/tests/test_even_block.py +132 -0
  89. firestarter-3.3.0b1/tests/test_every_row_encodes.py +41 -0
  90. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_firmware_install.py +37 -57
  91. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_flash4_erase_gate.py +10 -22
  92. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fw_list_failure_vs_empty.py +0 -9
  93. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fw_port_targeting_and_blind_install.py +12 -20
  94. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fw_release_gate.py +248 -69
  95. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fw_update_dead_endpoint.py +5 -4
  96. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fw_update_path_gate.py +25 -48
  97. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fw_version_guard.py +5 -16
  98. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_fwguard.py +13 -32
  99. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_hardware.py +143 -194
  100. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_hw_revision_gate.py +12 -151
  101. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_ic_layout.py +3 -17
  102. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_info_vpp_and_erase.py +37 -15
  103. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_jp5_gate.py +23 -22
  104. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_jumper_table.py +9 -4
  105. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_lock_status_class_partition.py +12 -14
  106. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_lock_status_cli.py +0 -5
  107. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_lock_status_wire.py +36 -41
  108. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_logging_utils.py +9 -9
  109. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_matrix_artifact.py +3 -36
  110. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_page_size_alignment_refusal.py +37 -41
  111. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_page_size_invariants.py +29 -65
  112. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_page_size_write_refusal.py +29 -31
  113. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_parse_devtest_issue.py +3 -4
  114. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_probe_spurious_setup_ack.py +17 -30
  115. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_programming_vcc_census.py +8 -108
  116. firestarter-3.3.0b1/tests/test_protect_flags_doc_measurements.py +32 -0
  117. firestarter-3.3.0b1/tests/test_protection_table_citations.py +56 -0
  118. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_protocol_not_implemented.py +2 -45
  119. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_protocol_not_implemented_production_path.py +24 -152
  120. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_provenance.py +0 -17
  121. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_pulse_us_override.py +38 -118
  122. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_py32_channel_gating.py +20 -114
  123. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_py32_dfu.py +7 -9
  124. firestarter-3.3.0b1/tests/test_pyusb_api_surface.py +155 -0
  125. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_sdp_capability.py +0 -3
  126. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_sdp_db_invariant.py +1 -41
  127. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_serial_characterization.py +27 -20
  128. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_serial_comm.py +52 -129
  129. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_session_lease.py +7 -14
  130. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_setup_refusals.py +13 -14
  131. firestarter-3.3.0b1/tests/test_setup_typed_refusals.py +109 -0
  132. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_submit.py +265 -897
  133. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_transport_counters.py +38 -31
  134. firestarter-3.3.0b1/tests/test_unsupported_frame_fallback.py +252 -0
  135. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_utils.py +8 -9
  136. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_validate_family_cmd.py +0 -6
  137. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_validate_oracle.py +27 -137
  138. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_validated_parts_regression_surface.py +51 -43
  139. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_vcc_margin_rail.py +35 -49
  140. firestarter-3.3.0b1/tests/test_vpp_decode_table.py +50 -0
  141. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_vpp_rail_classification.py +3 -211
  142. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_wire_dict_equivalence.py +11 -26
  143. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_blank_guard.py +107 -275
  144. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_blank_guard_pinning.py +20 -18
  145. firestarter-3.3.0b1/tests/test_write_budget.py +52 -0
  146. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_guard_port_pinning.py +18 -52
  147. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_progress.py +56 -129
  148. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_response_budget.py +164 -267
  149. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_skip_erase_0x0d.py +2 -8
  150. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_skip_sdp_unlock.py +34 -86
  151. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_write_verify.py +77 -213
  152. firestarter-3.2.7b2/firestarter.egg-info/scm_version.json +0 -8
  153. firestarter-3.2.7b2/tests/golden/build_db_part_specific_constants.json +0 -19
  154. firestarter-3.2.7b2/tests/test_build_db_constant_census.py +0 -138
  155. firestarter-3.2.7b2/tests/test_chip_database_field_inventory.py +0 -401
  156. firestarter-3.2.7b2/tests/test_chip_test_timing.py +0 -243
  157. firestarter-3.2.7b2/tests/test_decoder.py +0 -668
  158. firestarter-3.2.7b2/tests/test_dev_group_channel_gating.py +0 -331
  159. firestarter-3.2.7b2/tests/test_even_block.py +0 -198
  160. firestarter-3.2.7b2/tests/test_protect_flags_doc_measurements.py +0 -298
  161. firestarter-3.2.7b2/tests/test_protection_table_citations.py +0 -141
  162. firestarter-3.2.7b2/tests/test_pyusb_api_surface.py +0 -209
  163. firestarter-3.2.7b2/tests/test_vpp_decode_table.py +0 -59
  164. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/LICENSE +0 -0
  165. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/MANIFEST.in +0 -0
  166. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/README.md +0 -0
  167. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/__init__.py +0 -0
  168. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/address_parser.py +0 -0
  169. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/avr_tool.py +0 -0
  170. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/chip_test.py +0 -0
  171. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/compare.py +0 -0
  172. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/config.py +0 -0
  173. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/dip24_2816_erase_gate.py +0 -0
  174. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/erase_support.py +0 -0
  175. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/flash4_erase_gate.py +0 -0
  176. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/jp5_gate.py +0 -0
  177. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/lock_status.py +0 -0
  178. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/log_capture.py +0 -0
  179. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/logging_utils.py +0 -0
  180. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/main.py +0 -0
  181. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/protection_readability.py +0 -0
  182. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/py32_dfu.py +0 -0
  183. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/sdp_capability.py +0 -0
  184. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/sdp_honesty.py +0 -0
  185. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/submit.py +0 -0
  186. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/timestamps.py +0 -0
  187. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/transport_counters.py +0 -0
  188. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/utils.py +0 -0
  189. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter/write_blank_guard.py +0 -0
  190. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter.egg-info/dependency_links.txt +0 -0
  191. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter.egg-info/entry_points.txt +0 -0
  192. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter.egg-info/requires.txt +0 -0
  193. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/firestarter.egg-info/top_level.txt +0 -0
  194. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/pyproject.toml +0 -0
  195. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/setup.cfg +0 -0
  196. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/__init__.py +0 -0
  197. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/part_number_delta.json +0 -0
  198. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/at28c256-full-all-ok-sdp.json +0 -0
  199. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/attr01-status-axis-transport-fault.json +0 -0
  200. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/gh20-at28c256-fail.json +0 -0
  201. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/gh23-w27e257-fail.json +0 -0
  202. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/gh28-m27c512-fail.json +0 -0
  203. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/gh47-sst27sf512-pass.json +0 -0
  204. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/m27c512-full-all-ok.json +0 -0
  205. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/m27c512-full-blank-check-bad.json +0 -0
  206. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/m27c512-full-canonical-name.json +0 -0
  207. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/m27c512-full-comma-joined-name.json +0 -0
  208. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/m27c512-full-runs-1.json +0 -0
  209. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/prune03-synthesized-fingerprint-match.json +0 -0
  210. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/sst27sf512-full-all-ok.json +0 -0
  211. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/sst27sf512-six-step-readback-gated.json +0 -0
  212. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/sst27sf512-six-step.json +0 -0
  213. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/synthetic-arm4-empty-results.json +0 -0
  214. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/synthetic-arm4-no-ok.json +0 -0
  215. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/uv-slot-write-pass.json +0 -0
  216. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/reports/w27e257-full-all-ok.json +0 -0
  217. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/shape_ids.json +0 -0
  218. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/fixtures/synthetic_nonzero_chip_id.py +0 -0
  219. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_address_parser.py +0 -0
  220. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_build_db_interpret_timing.py +0 -0
  221. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_build_db_pinout_fork.py +0 -0
  222. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_build_db_sram_non_volatile.py +0 -0
  223. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_click_group_gate_hook.py +0 -0
  224. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_dfu_opcode_anchors.py +0 -0
  225. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_endpoint_constants.py +0 -0
  226. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_eprom_database.py +0 -0
  227. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_error_code_seam.py +0 -0
  228. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_extra_chips_supplement.py +0 -0
  229. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_log_capture.py +0 -0
  230. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_protection_resolution.py +0 -0
  231. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_protection_status_catalog.py +0 -0
  232. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_py32_flash_map_host.py +0 -0
  233. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_py32_pyusb_absent.py +0 -0
  234. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_sdp_honesty.py +0 -0
  235. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_timestamps.py +0 -0
  236. {firestarter-3.2.7b2 → firestarter-3.3.0b1}/tests/test_uv_mask.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firestarter
3
- Version: 3.2.7b2
3
+ Version: 3.3.0b1
4
4
  Summary: EPROM programmer software for the Relatively Universal ROM Programmer
5
5
  Author-email: Henrik Olsson <henols@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/henols/firestarter_app
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.2.7b2'
22
- __version_tuple__ = version_tuple = (3, 2, 7, 'b2')
21
+ __version__ = version = '3.3.0b1'
22
+ __version_tuple__ = version_tuple = (3, 3, 0, 'b1')
23
23
 
24
- __commit_id__ = commit_id = 'g30e9def'
24
+ __commit_id__ = commit_id = 'g927ad2b'
@@ -59,23 +59,16 @@ def is_prerelease_build() -> bool:
59
59
  """True when this build came off `beta` (its own version is a pre-release).
60
60
 
61
61
  Fails **closed**: an unparseable version is treated as stable, so a gated
62
- feature stays hidden rather than leaking. This matches
63
- `_maybe_auto_route_to_pre`, which also treats `InvalidVersion` as
64
- not-a-prerelease. In practice `__version__` is a literal in the package and
65
- is always valid PEP 440; dev versions (`2.0.7_dev`) parse as pre-releases and
66
- therefore keep gated features enabled while working from a checkout.
62
+ feature stays hidden rather than leaking. `_maybe_auto_route_to_pre` uses
63
+ this function, so it treats an unparseable version the same way. In
64
+ practice `__version__` is a literal in the package and is always valid
65
+ PEP 440; dev versions (`2.0.7_dev`) parse as pre-releases and therefore
66
+ keep gated features enabled while working from a checkout.
67
67
  """
68
- try:
69
- from packaging.version import InvalidVersion, Version
68
+ import firestarter as _pkg
69
+ from firestarter.fw_release_gate import is_prerelease_version
70
70
 
71
- import firestarter as _pkg
72
-
73
- try:
74
- return bool(Version(_pkg.__version__).is_prerelease)
75
- except InvalidVersion:
76
- return False
77
- except ImportError: # pragma: no cover — packaging is a hard dependency
78
- return False
71
+ return is_prerelease_version(_pkg.__version__) is True
79
72
 
80
73
 
81
74
  def is_board_available(board: str | None) -> bool:
@@ -9,7 +9,7 @@ Single chokepoint between CLI dispatch and the EPROM database lookup/conversion.
9
9
 
10
10
  from typing import Any
11
11
 
12
- from firestarter.database import ROM_CE, ROM_OE, EpromDatabase
12
+ from firestarter.database import ROM_CE, ROM_OE, EpromDatabase, pin_conversions
13
13
  from firestarter.exceptions import (
14
14
  AmbiguousChipNameError,
15
15
  ChipNotFoundError,
@@ -18,6 +18,13 @@ from firestarter.exceptions import (
18
18
 
19
19
  _NON_ZERO_WIRE_FIELDS = ("memory-size", "pin-count", "vpp_mv")
20
20
 
21
+ # The 0x10 (Intel 28F) handler routes 12V to socket pin 1 (CTRL_VPP_P1_ENABLE).
22
+ _INTEL_28F_ALGORITHM = 0x10
23
+
24
+ # The shield supplies a fixed 5V VCC. Must agree with SHIELD_MIN_VCC_MV in
25
+ # tools/build_db.py.
26
+ _SHIELD_MIN_VCC_MV = 4500
27
+
21
28
 
22
29
  def _qualified_choices(name: str, db: EpromDatabase) -> list[str]:
23
30
  """The row `name` selects first, then each other row it selects that
@@ -115,13 +122,22 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
115
122
  "a chip entry must specify a non-zero 'algorithm' to be programmed"
116
123
  )
117
124
 
125
+ # A 3V part runs out of its rating on the shield's 5V supply. A user
126
+ # override that marks such a row supported is refused here, before any wire.
127
+ vcc_mv = raw_config.get("electrical", {}).get("vcc_mv")
128
+ if isinstance(vcc_mv, int) and 0 < vcc_mv < _SHIELD_MIN_VCC_MV:
129
+ raise ChipNotImplementedError(
130
+ f"{name}: the chip's operating VCC is {vcc_mv / 1000:g}V, but the "
131
+ "shield supplies 5V. The programmer refuses the chip."
132
+ )
133
+
118
134
  full = db.get_eprom(name)
119
135
  data = db.convert_to_programmer(full) if full else None
120
136
  if not data:
121
137
  raise ChipNotFoundError(name)
122
138
 
123
139
  # The firmware refuses a memory command whose memory-size, pin-count or
124
- # vpp_mv is 0, with the same 'Bad JSON' answer that old firmware gives.
140
+ # vpp_mv is 0 (MSG_ERR_BAD_COMMAND), so the port walk sees no programmer.
125
141
  # No shipped row has a 0 here; a user database override can.
126
142
  zero_fields = [key for key in _NON_ZERO_WIRE_FIELDS if not data.get(key)]
127
143
  if zero_fields:
@@ -148,4 +164,13 @@ def resolve_chip(name: str, db: EpromDatabase | None = None) -> dict[str, Any]:
148
164
  f"{1 << len(bus)} of its {data['memory-size']} bytes. The programmer "
149
165
  "cannot address the rest of the chip."
150
166
  )
167
+ # The 0x10 handler puts 12V on socket pin 1, so a 0x10 chip whose bus uses
168
+ # that pin (A18 on the SST28SF040 family) would take 12V on an address
169
+ # line. Only a 32-pin chip has a pin there: a smaller chip sits
170
+ # bottom-aligned, and none of its pins converts to the socket pin 1 line.
171
+ if bus and algorithm == _INTEL_28F_ALGORITHM and pin_conversions[32][1] in bus:
172
+ raise ChipNotImplementedError(
173
+ f"{name}: algorithm 0x10 puts 12V on socket pin 1, but the chip's pin "
174
+ "map uses pin 1 as an address line. The programmer refuses the chip."
175
+ )
151
176
  return data
@@ -21,6 +21,7 @@ import click.shell_completion
21
21
  from rich.console import Console
22
22
 
23
23
  from firestarter import __version__ as version
24
+ from firestarter import command_frame as cf
24
25
  from firestarter import (
25
26
  dip24_2816_erase_gate,
26
27
  flash4_erase_gate,
@@ -62,6 +63,7 @@ from firestarter.chip_test import (
62
63
  sdp_hold_state,
63
64
  sdp_oracle_applicable,
64
65
  )
66
+ from firestarter.command_codec import CommandEncodeError
65
67
  from firestarter.config import ConfigManager, get_config_dir
66
68
  from firestarter.constants import (
67
69
  CONFIG_R1_MAX,
@@ -85,6 +87,7 @@ from firestarter.exceptions import (
85
87
  ChipNotFoundError,
86
88
  ChipNotImplementedError,
87
89
  EpromOperationError,
90
+ FirmwareFrameVersionError,
88
91
  FirmwareOperationError,
89
92
  FirmwareOutdatedError,
90
93
  HardwareOperationError,
@@ -98,7 +101,7 @@ from firestarter.exceptions import (
98
101
  )
99
102
  from firestarter.firmware import FIRMWARE_VERSION_RE, FirmwareManager
100
103
  from firestarter.fw_release_gate import require_installable_release
101
- from firestarter.hardware import HardwareManager
104
+ from firestarter.hardware import HardwareManager, bandgap_from_measured_vcc
102
105
  from firestarter.lock_status import (
103
106
  classify_protection_response,
104
107
  exit_code_for_class,
@@ -238,6 +241,8 @@ def map_typed_errors(f: Callable[..., Any]) -> Callable[..., Any]:
238
241
  return f(*args, **kwargs)
239
242
  except ChipNotFoundError as e:
240
243
  raise click.ClickException(str(e)) from e
244
+ except FirmwareFrameVersionError as e:
245
+ raise click.ClickException(f"Command frame version mismatch: {e}") from e
241
246
  except FirmwareOutdatedError as e:
242
247
  raise click.ClickException(f"Firmware outdated: {e}") from e
243
248
  except (SerialError, SerialTimeoutError) as e:
@@ -269,6 +274,11 @@ def map_typed_errors(f: Callable[..., Any]) -> Callable[..., Any]:
269
274
  # make this host-voiced refusal (write_blank_guard.py) read as a
270
275
  # hardware fault instead of an input-validation refusal.
271
276
  raise click.ClickException(str(e)) from e
277
+ except CommandEncodeError as e:
278
+ # The host refused the command before any byte reached the port.
279
+ # Above the EpromOperationError arm: "Programmer error" would read
280
+ # as a hardware fault for an input the frame cannot carry.
281
+ raise click.ClickException(f"Refused before sending: {e}") from e
272
282
  except EpromOperationError as e:
273
283
  raise click.ClickException(f"Programmer error: {e}") from e
274
284
  except Pin1HazardRefusedError as e:
@@ -343,29 +353,15 @@ def _maybe_auto_route_to_pre(args: object) -> None:
343
353
  or getattr(args, "stable", False)
344
354
  ):
345
355
  return
346
- try:
347
- from packaging.version import InvalidVersion, Version
348
-
349
- import firestarter as _pkg
350
-
351
- try:
352
- if Version(_pkg.__version__).is_prerelease:
353
- args.pre = True # type: ignore[attr-defined]
354
- helper_logger.info(
355
- "Beta app detected — defaulting to --pre. "
356
- "Use --firmware-version X.Y.Z to pin a stable version."
357
- )
358
- except InvalidVersion:
359
- # Deliberate fail-OPEN, and deliberately the opposite polarity to
360
- # fw_release_gate, which fails CLOSED on the same unreadable input.
361
- # The asymmetry is intended: the worst case here is that the wrong
362
- # channel is picked, which the gate then judges on its merits; the
363
- # worst case there is a board flashed with firmware this host cannot
364
- # drive, recoverable only through the bootloader. Do not "align"
365
- # these two without re-reading both worst cases.
366
- pass
367
- except ImportError:
368
- pass
356
+ # An unreadable version is not a pre-release here: the worst case is the
357
+ # wrong channel, which fw_release_gate then judges and refuses (it fails
358
+ # CLOSED on the same input). Do not "align" the two polarities.
359
+ if is_prerelease_build():
360
+ args.pre = True # type: ignore[attr-defined]
361
+ helper_logger.info(
362
+ "Beta app detected — defaulting to --pre. "
363
+ "Use --firmware-version X.Y.Z to pin a stable version."
364
+ )
369
365
 
370
366
 
371
367
  def _build_op_flags(
@@ -381,6 +377,7 @@ def _build_op_flags(
381
377
  # SDP auto-set condition inside write() below -- flipping this default
382
378
  # invalidates the removal-safety argument the `dev sdp` removal rests on.
383
379
  skip_sdp_unlock: bool = False,
380
+ skip_final_verify: bool = False,
384
381
  input_enable: bool | None = None,
385
382
  chip_disable: bool | None = None,
386
383
  ) -> int:
@@ -410,6 +407,7 @@ def _build_op_flags(
410
407
  verbose,
411
408
  skip_erase=skip_erase,
412
409
  skip_sdp_unlock=skip_sdp_unlock,
410
+ skip_final_verify=skip_final_verify,
413
411
  )
414
412
  if input_enable is not None:
415
413
  flags |= 0 if input_enable else FLAG_OUTPUT_ENABLE
@@ -947,6 +945,9 @@ def write(
947
945
  vpe_as_vpp=vpe_as_vpp,
948
946
  skip_erase=skip_erase,
949
947
  skip_sdp_unlock=skip_sdp_unlock,
948
+ # The read-back below compares every byte, so the board's own
949
+ # final verify pass would only repeat it.
950
+ skip_final_verify=verify,
950
951
  ),
951
952
  # Translate Click's `None` ("--pulse-us not supplied") into
952
953
  # write_eprom's own integer sentinel (0 means "use the database
@@ -1062,8 +1063,7 @@ def _address_size_refusal_exit_code(
1062
1063
  """Refuse a negative start address or a size below 1 byte before the
1063
1064
  serial port opens.
1064
1065
 
1065
- The firmware refuses both at setup, with the same 'Bad JSON' answer that
1066
- firmware too old for this CLI gives, so without this check the operator
1066
+ The firmware refuses both at setup, so without this check the operator
1067
1067
  sees only "no programmer found". Returns 2 to refuse with, or `None`.
1068
1068
  A malformed string is not refused here: `_setup_operation`'s own parse
1069
1069
  handling still owns that error.
@@ -1606,15 +1606,22 @@ def hw(app: AppContext) -> None:
1606
1606
 
1607
1607
 
1608
1608
  _BANDGAP_NOMINAL_MV = 1100
1609
+ # A plausibility bound for a 5 V supply pin, not a wire limit: the host sends
1610
+ # the bandgap, not the reading. A reading above it is a typo, refused before
1611
+ # any port opens.
1612
+ _CAL_VCC_MAX_V = 10.0
1609
1613
 
1610
1614
 
1611
1615
  @cli.command(name="cal")
1612
1616
  @click.option(
1613
1617
  "--vcc",
1614
- type=click.FloatRange(min=0, min_open=True),
1618
+ type=click.FloatRange(min=0, min_open=True, max=_CAL_VCC_MAX_V),
1615
1619
  default=None,
1616
1620
  metavar="VOLTS",
1617
- help="Calibrate using a multimeter reading of the 5V pin, e.g. --vcc 5.09",
1621
+ help=(
1622
+ "Calibrate using a multimeter reading of the 5V pin, e.g. --vcc 5.09. "
1623
+ f"At most {_CAL_VCC_MAX_V:g} V."
1624
+ ),
1618
1625
  )
1619
1626
  @click.option(
1620
1627
  "--reset",
@@ -1654,21 +1661,43 @@ def cal(app: AppContext, vcc: float | None, reset: bool) -> None:
1654
1661
  if vcc is not None:
1655
1662
  measured_mv = int(round(vcc * 1000))
1656
1663
  before = app.hardware_manager.read_calibration()
1657
- if not app.hardware_manager.calibrate_bandgap(measured_mv):
1664
+ if before is None:
1665
+ click.echo(
1666
+ "Could not read the calibration. The firmware may be older than "
1667
+ "this CLI."
1668
+ )
1669
+ sys.exit(1)
1670
+ bandgap_adc = before["bandgap_adc"]
1671
+ if bandgap_adc == 0:
1672
+ click.echo("This board has no bandgap calibration (py32f071).")
1673
+ sys.exit(1)
1674
+ bandgap_mv = bandgap_from_measured_vcc(measured_mv, bandgap_adc)
1675
+ if bandgap_mv is None:
1658
1676
  click.echo(
1659
1677
  "Calibration refused. The result was outside 1000-1200 mV, which "
1660
1678
  "means the reading does not match this board. Check that the "
1661
- "multimeter is on the 5V pin and that the value is in volts. "
1662
- "A py32f071 board has no bandgap calibration and always "
1663
- "refuses it."
1679
+ "multimeter is on the 5V pin and that the value is in volts."
1664
1680
  )
1665
1681
  sys.exit(1)
1682
+ if not app.hardware_manager.set_bandgap_mv(bandgap_mv):
1683
+ sys.exit(1)
1684
+ # Read the value back over a new connection, so the report is what the
1685
+ # board stored, not what this host sent.
1666
1686
  after = app.hardware_manager.read_calibration()
1667
- if before is not None and after is not None:
1687
+ if after is None:
1688
+ click.echo(
1689
+ f"Sent {bandgap_mv} mV, but could not read the calibration back. "
1690
+ "Run 'firestarter cal' to check it."
1691
+ )
1692
+ sys.exit(1)
1693
+ if after["bandgap_mv"] != bandgap_mv:
1668
1694
  click.echo(
1669
- f"Reference: {before['bandgap_mv']} mV -> {after['bandgap_mv']} mV"
1695
+ f"Calibration not stored: the board reports {after['bandgap_mv']} "
1696
+ f"mV, expected {bandgap_mv} mV."
1670
1697
  )
1671
- _echo_calibration_effect(after["bandgap_mv"])
1698
+ sys.exit(1)
1699
+ click.echo(f"Reference: {before['bandgap_mv']} mV -> {after['bandgap_mv']} mV")
1700
+ _echo_calibration_effect(after["bandgap_mv"])
1672
1701
  sys.exit(0)
1673
1702
 
1674
1703
  current = app.hardware_manager.read_calibration()
@@ -2285,16 +2314,22 @@ if _DEV_TOOLS_ENABLED:
2285
2314
  @click.option(
2286
2315
  "--read-settling",
2287
2316
  "read_settling_us",
2288
- type=int,
2317
+ type=click.IntRange(0, cf.READ_TIMING_MAX_US),
2289
2318
  default=0,
2290
- help="Address-settling delay before /CE assert (µs; 0=firmware default 0µs).",
2319
+ help=(
2320
+ "Address-settling delay before /CE assert "
2321
+ f"(µs, 0..{cf.READ_TIMING_MAX_US}; 0=firmware default 0µs)."
2322
+ ),
2291
2323
  )
2292
2324
  @click.option(
2293
2325
  "--read-strobe",
2294
2326
  "read_strobe_us",
2295
- type=int,
2327
+ type=click.IntRange(0, cf.READ_TIMING_MAX_US),
2296
2328
  default=0,
2297
- help="/CE read-strobe pulse width (µs; 0=firmware default 3µs).",
2329
+ help=(
2330
+ "/CE read-strobe pulse width "
2331
+ f"(µs, 0..{cf.READ_TIMING_MAX_US}; 0=firmware default 3µs)."
2332
+ ),
2298
2333
  )
2299
2334
  @click.pass_obj
2300
2335
  @map_typed_errors
@@ -12,6 +12,7 @@ import logging
12
12
  import struct
13
13
  from typing import Any, List # noqa: UP035
14
14
 
15
+ from firestarter.command_frame import REFUSALS
15
16
  from firestarter.constants import (
16
17
  COMMAND_NAMES,
17
18
  REVISION_0,
@@ -29,22 +30,58 @@ from firestarter.messages import (
29
30
  DEBUG_CATALOG,
30
31
  MSG_DATA_CHUNK,
31
32
  MSG_DEBUG,
33
+ MSG_ERR_BAD_COMMAND,
34
+ MSG_ERR_VPP_HIGH_MV,
32
35
  MSG_INFO_CMD,
33
36
  MSG_INFO_HW,
34
37
  MSG_INFO_PHYSICAL_HW,
38
+ MSG_INFO_REGISTERS,
35
39
  MSG_OK_CFG,
36
40
  MSG_OK_REV,
41
+ MSG_WARN_VPP_HIGH_MV,
42
+ MSG_WARN_VPP_LOW_MV,
37
43
  SEVERITY_LABEL,
38
44
  MessageDef,
39
45
  )
40
46
 
41
47
  logger = logging.getLogger("Codec")
42
48
 
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}
49
+ # MSG_INFO_REGISTERS views (firmware sync: dev_tools.cpp DT_VIEW_*).
50
+ _REGISTER_VIEW_REG_FIRESTARTER = 1
51
+ _REGISTER_VIEW_ADDR = 2
52
+
53
+
54
+ def volts(mv: int) -> str:
55
+ """`'12.449 V'` for a millivolt value: every millivolt the firmware sent."""
56
+ return f"{mv / 1000:.3f} V"
57
+
58
+
59
+ def _register_table(name: str, value: int, bits: int) -> list[str]:
60
+ """One register as the three lines `dev reg` / `dev addr` print: the
61
+ value, the bit header and the bit row, D(bits-1) first."""
62
+ header = "|D8" if bits == 9 else ""
63
+ row = "".join(f"| {(value >> i) & 1}" for i in range(bits - 1, -1, -1))
64
+ return [
65
+ f"{name}: 0x{value:02X}",
66
+ f"{header}|D7|D6|D5|D4|D3|D2|D1|D0|",
67
+ f"{row}|",
68
+ ]
69
+
70
+
71
+ def _render_registers(msb: int, lsb: int, ctrl: int, remapped: int, view: int) -> str:
72
+ """The bit tables for MSG_INFO_REGISTERS. The firmware sends raw values;
73
+ the table layout is the one the firmware used to print."""
74
+ lines = _register_table("MSB", msb, 8) + _register_table("LSB", lsb, 8)
75
+ if view == _REGISTER_VIEW_ADDR:
76
+ lines += _register_table("TOP addr", ctrl, 8)
77
+ lines += _register_table("CTRL remapped", remapped, 8)
78
+ elif view == _REGISTER_VIEW_REG_FIRESTARTER:
79
+ lines += _register_table("CTRL", ctrl, 9)
80
+ lines += _register_table("CTRL remapped", remapped, 8)
81
+ else:
82
+ lines += _register_table("CTRL", ctrl & 0xFF, 8)
83
+ return "\n".join(lines)
84
+
48
85
 
49
86
  # REVISION_* byte → silkscreen-string mapping for MSG_OK_REV rendering.
50
87
  # Mirrors firmware enum at firestarter/include/rurp_shield.h. Lookup-via-dict.get()
@@ -78,13 +115,10 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
78
115
  (the override is in effect; the second field is what the board detected)
79
116
 
80
117
  P-03 MSG_OK_CFG — params[0]=r1 u32, params[1]=r2 u32, params[2]=override u8,
81
- params[3]=bandgap_mv u16 (absent on firmware before 3.1.0b4)
82
- override==0xFF → "R1: {r1}, R2: {r2}, Bandgap: {n} mV"
83
- override!=0xFF → "R1: {r1}, R2: {r2}, Override HW: {silkscreen_str}, Bandgap: {n} mV"
84
- The Bandgap clause is always present when the ack carries the field:
85
- HardwareManager.read_calibration keys on it. The 9-byte form from older
86
- firmware has no bandgap, so it renders without the Bandgap clause, and
87
- read_calibration reports that firmware as too old.
118
+ params[3]=bandgap_mv u16, params[4]=bandgap_adc u16
119
+ override==0xFF → "R1: {r1}, R2: {r2}, Bandgap: {n} mV, Bandgap count: {c}"
120
+ override!=0xFF → "R1: {r1}, R2: {r2}, Override HW: {silkscreen_str},
121
+ Bandgap: {n} mV, Bandgap count: {c}"
88
122
  Override clause now routes through
89
123
  _REVISION_SILKSCREEN so the same byte that surfaces as "Rev 2.0-class"
90
124
  via MSG_OK_REV no longer surfaces as "Rev2" on the adjacent ack line.
@@ -107,7 +141,7 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
107
141
  eff_str = _REVISION_SILKSCREEN.get(effective, f"Rev{effective}")
108
142
  return f"{eff_str}, Detected HW: {phys_str}"
109
143
 
110
- if msg_id == MSG_OK_CFG and len(params) in (3, 4):
144
+ if msg_id == MSG_OK_CFG and len(params) == 5:
111
145
  r1, r2, override = params[0], params[1], params[2]
112
146
  head = f"R1: {r1}, R2: {r2}"
113
147
  if override != 0xFF:
@@ -117,12 +151,7 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
117
151
  # No-space "Rev{n}" fallback mirrors the MSG_OK_REV branch shape.
118
152
  override_str = _REVISION_SILKSCREEN.get(override, f"Rev{override}")
119
153
  head += f", Override HW: {override_str}"
120
- if len(params) == 3:
121
- # Firmware before 3.1.0b4 sends no bandgap field.
122
- return head
123
- # v1.43 appended the calibrated reference. Rendered last so the
124
- # override clause keeps the position every prior release put it in.
125
- return f"{head}, Bandgap: {params[3]} mV"
154
+ return f"{head}, Bandgap: {params[3]} mV, Bandgap count: {params[4]}"
126
155
 
127
156
  # Silkscreen-aware rendering for the two
128
157
  # boot-time INFO surfaces that carry the same revision byte as
@@ -136,6 +165,22 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
136
165
  byte = params[0]
137
166
  return f"Physical HW: {_REVISION_SILKSCREEN.get(byte, f'Rev{byte}')}"
138
167
 
168
+ if msg_id == MSG_ERR_BAD_COMMAND and len(params) == 1:
169
+ code = params[0]
170
+ return f"Command refused: {REFUSALS.get(code, f'reason {code}')}"
171
+
172
+ if msg_id == MSG_INFO_REGISTERS and len(params) == 5:
173
+ return _render_registers(*params)
174
+
175
+ # The firmware sends millivolts; show volts. The catalog text (mV) is the
176
+ # fallback for any other shape.
177
+ if msg_id in (MSG_WARN_VPP_LOW_MV, MSG_WARN_VPP_HIGH_MV, MSG_ERR_VPP_HIGH_MV):
178
+ if len(params) == 2:
179
+ measured, expected = params
180
+ relation = "<" if msg_id == MSG_WARN_VPP_LOW_MV else ">"
181
+ state = "low" if msg_id == MSG_WARN_VPP_LOW_MV else "high"
182
+ return f"VPP is {state}: {volts(measured)} {relation} {volts(expected)}"
183
+
139
184
  if msg_id == MSG_INFO_CMD and len(params) == 1:
140
185
  cmd = params[0]
141
186
  name = COMMAND_NAMES.get(cmd)
@@ -244,27 +289,18 @@ def decode_id_frame(frame_len: int, body: bytes) -> LogMessage | None:
244
289
  # entries carry param_bytes == -1 in the catalog; for those we
245
290
  # cannot pre-validate, but _decode_param will surface any overrun
246
291
  # via IndexError below.
247
- legacy_len = _LEGACY_PARAM_BYTES.get(msg_id)
248
- is_legacy = legacy_len is not None and len(params_bytes) == legacy_len
249
- if (
250
- entry.param_bytes >= 0
251
- and len(params_bytes) != entry.param_bytes
252
- and not is_legacy
253
- ):
292
+ if entry.param_bytes >= 0 and len(params_bytes) != entry.param_bytes:
254
293
  logger.warning(
255
294
  f"Param shape mismatch for ID 0x{msg_id:02x} ({entry.name}): "
256
295
  f"expected {entry.param_bytes} bytes, got {len(params_bytes)}"
257
296
  )
258
297
  return None
259
298
 
260
- # Decode each param per the catalog grammar. A legacy short form stops
261
- # at the end of its shorter param region.
299
+ # Decode each param per the catalog grammar.
262
300
  values: List[Any] = [] # noqa: UP006
263
301
  cursor = 0
264
302
  try:
265
303
  for ptype, _prender in entry.params:
266
- if is_legacy and cursor >= len(params_bytes):
267
- break
268
304
  value, cursor = _decode_param(ptype, params_bytes, cursor)
269
305
  values.append(value)
270
306
  except (IndexError, struct.error, ValueError) as exc:
@@ -295,15 +331,19 @@ def decode_id_frame(frame_len: int, body: bytes) -> LogMessage | None:
295
331
 
296
332
  # Extract raw-bytes payload for MSG_DATA_CHUNK (W-04) so the chip-read
297
333
  # loop can obtain the chip data without a second read call.
298
- chunk_payload = None
334
+ payload = None
299
335
  if (
300
336
  msg_id == MSG_DATA_CHUNK
301
337
  and values
302
338
  and isinstance(values[0], (bytes, bytearray))
303
339
  ):
304
- chunk_payload = bytes(values[0])
340
+ payload = bytes(values[0])
305
341
 
306
342
  severity_label = SEVERITY_LABEL.get(entry.severity, f"SEV{entry.severity}")
307
343
  return LogMessage(
308
- severity=severity_label, text=text, id=msg_id, payload=chunk_payload
344
+ severity=severity_label,
345
+ text=text,
346
+ id=msg_id,
347
+ payload=payload,
348
+ params=tuple(values),
309
349
  )