358 Commits

Author SHA1 Message Date
NawfalMotii79
2a9f7161a0 Merge pull request #136 from joyshmitz/fix/develop-ci-green-restore
fix(ci): restore develop CI green — ruff T20 exemption + ADAR1000 setter regex extension
2026-05-13 23:29:47 +01:00
Serhii
49055a8bf1 fix(ci): restore develop CI green — ruff T20 exemption + ADAR1000 setter regex extension
Two independent root causes landed together in the recent develop merge
burst, leaving CI red on every commit since. Both fixes are narrow parser/
config updates; no code logic changes.

1. Ruff T201 violation in 8_Utils/Python/LUT.py:24

   The recent restoration of `print(f"8'd{val},")` (replacing `pass`) made
   the LUT generator functional again, but the file is not in pyproject's
   per-file-ignores so ruff's T20 rule flags it. Add a narrow per-file-
   ignore for T20 specifically scoped to LUT.py.

2. Five tests in TestTier1Adar1000ChannelRegisterRoundTrip

   The recent SPI/ADC error-propagation refactor on ADAR1000_Manager.cpp
   changed the four setters adarSet{Rx,Tx}{Phase,VgaGain} from `void` to
   `bool` returns AND introduced the `ok = setter(args) && ok` error-chain
   idiom at internal call sites. The test class's parser regexes were
   authored for the pre-refactor convention.

   Two regex extensions:

     * Body parser: `void\s+...` -> `(?:void|bool)\s+...` so bodies are
       found under either return-type convention.
     * Caller finder: `\)\s*;` -> `\)(?:\s*&&\s*\w+)*\s*;` so the
       error-chain idiom is matched alongside standalone calls.

   Body extraction logic, REG_CHn_XXX + <expr> regex, symbolic AST
   evaluation, round-trip strict-equality, and stride detection are all
   unchanged. The two regex extensions only acknowledge the new C++
   conventions introduced by the SPI/ADC refactor.

Verified locally:
- `uv run ruff check .` returns 0 errors.
- `uv run pytest 9_Firmware/tests/cross_layer/test_cross_layer_contract.py`
  passes 51 tests + 5 skipped (Tier2VerilogCosim local-only, requires
  iverilog which is installed in CI).
- `uv run pytest 9_Firmware/9_3_GUI/test_v7.py` passes 83 tests + 3
  skipped (no regression).
2026-05-07 07:50:34 +03:00
NawfalMotii79
34e8084a08 Merge pull request #104 from joyshmitz/test/cross-layer-status-field-layout
test(cross-layer): enforce status word field positions match Verilog concat layout
2026-05-06 20:17:38 +01:00
NawfalMotii79
3842c77390 Merge pull request #120 from joyshmitz/fix/smoke-test-self-test-decode
fix(smoke-test): decode self-test results from dedicated status fields
2026-05-06 20:16:46 +01:00
NawfalMotii79
8e89da7bf8 Merge pull request #131 from soufianebouaddis/fix/adar1000-comm-status-propagation
fix: propagate SPI/ADC communication failures in ADAR1000_Manager
2026-05-06 20:12:37 +01:00
NawfalMotii79
42987170e8 Merge pull request #127 from Formatted/fix/three-bugs-onto-develop
fix: three utility bugs in compare, LUT, and triangular waveform scripts
2026-05-06 20:11:36 +01:00
SoufianeBouaddis
c4df8cb606 fix(adar1000): explicit (void) casts on intentional pulse-mode discards 2026-05-01 14:40:45 +01:00
SoufianeBouaddis
23c5f82753 fix: propagate SPI/ADC communication failures in ADAR1000_Manager 2026-04-30 17:43:11 +01:00
Jason
89e688e9a2 Merge remote-tracking branch 'origin/main' into develop 2026-04-27 13:10:10 +05:45
Formatted
2b5c6592df fix: three utility bugs in compare, LUT, and triangular waveform scripts
- compare.py: capture max_abs_error return values and add pass/fail
  check (was silently discarded, missing outlier detection)
- LUT.py: replace no-op pass with actual print of 8-bit Verilog values
- Gen_Triangular.py: fix plt.plot(2*n, y) -> plt.plot(t, x) scalar vs
  array bug that produced broken time-domain plot
2026-04-23 23:46:44 -06:00
NawfalMotii79
b0c2d70ce2 Add Mechanical parts
Add Mechanical parts (Waveguide and heat sinks)
2026-04-23 01:06:51 +01:00
NawfalMotii79
8bd880ce4c Added BOM and Gerbers
BOM and Gerbers, ready to be sent to PCB manufacturer
2026-04-22 01:04:28 +01:00
NawfalMotii79
a8aefc4f61 Merge pull request #119 from NawfalMotii79/fix/mcu-fault-ack-emergency-clear
fix(mcu): FAULT_ACK USB command clears system_emergency_state (closes #83)
2026-04-21 23:11:42 +01:00
Serhii
470f68c370 fix(smoke-test): decode self-test results from dedicated status fields
smoke_test.py:154-155 extracted self-test flags/detail from
status.cfar_threshold — but that field carries the CFAR detection
threshold (opcode 0x03 readback), not self-test results.

RadarProtocol.parse_status_packet already exposes the correct fields
via status.self_test_flags and status.self_test_detail (radar_protocol.py:257,
word 5: {7'd0, self_test_busy, 8'd0, self_test_detail[7:0], 3'd0,
self_test_flags[4:0]}). test_GUI_V65_Tk.py:198 pins these fields as
the contract.

Result: smoke_test on live hardware would have decoded garbage (CFAR
threshold bits) as per-subsystem PASS/FAIL flags. Fix replaces the
two assignments and refreshes the stale "simplification" comment
that predated the dedicated status fields.

Regression: 137 passed, 3 skipped (test_v7.py + cross_layer). 8
self_test invariant tests in test_GUI_V65_Tk.py still pass.
2026-04-21 09:09:37 +03:00
Jason
5b84af68f6 fix(mcu): add FAULT_ACK command to clear system_emergency_state via USB (closes #83)
The volatile fix in the companion PR (#118) makes the safe-mode blink loop
escapable in principle, but no firmware path existed to actually clear
system_emergency_state at runtime — hardware reset was the only exit, which
fires the IWDG and re-energises the PA rails that Emergency_Stop() cut.

This change adds a FAULT_ACK command (opcode 0x40): the host sends an exact
4-byte CDC packet [0x40, 0x00, 0x00, 0x00]; USBHandler detects it regardless
of USB state and sets fault_ack_received; the blink loop checks the flag each
250 ms iteration and clears system_emergency_state, allowing a controlled
operator-acknowledged recovery without triggering a watchdog reset.

Detection is guarded to exact 4-byte packets only. Scanning larger packets
for the subsequence would false-trigger on the IEEE 754 big-endian encoding
of 2.0 (0x4000000000000000), which starts with the same 4 bytes and can
appear in normal settings doubles.

FAULT_ACK is excluded from the FPGA opcode enum to preserve the
Python/Verilog bidirectional contract test; contract_parser.py reads the
new MCU_ONLY_OPCODES frozenset in radar_protocol.py to filter it.

7 new test vectors in test_gap3_fault_ack_clears_emergency.c cover:
detection, loop exit, loop hold without ack, settings false-positive
immunity, truncated packet, wrong opcode, and multi-iteration sequence.

Reported-by: shaun0927 (Junghwan) <https://github.com/shaun0927>
2026-04-21 03:57:55 +05:45
Jason
846a0debe8 Merge pull request #118 from NawfalMotii79/fix/mcu-volatile-emergency-state-agc-holdoff
fix(mcu): volatile emergency state + AGC holdoff zero-guard (closes #83)
2026-04-21 00:57:08 +03:00
Jason
e979363730 fix(mcu): volatile emergency state + AGC holdoff zero-guard (closes #83)
Bug 1 (main.cpp:630): system_emergency_state lacked volatile. Under -O1+
the compiler is permitted to hoist the read outside the blink loop, making
while (system_emergency_state) unconditionally infinite. Once entered, the
only escape was the 4 s IWDG timeout — which resets the MCU and
re-energizes the PA rails that Emergency_Stop() explicitly cut. Marking the
variable volatile forces a memory read on every iteration so an external
clear (ISR, USB command, manual reset) can break the loop correctly.

Bug 2 (ADAR1000_AGC.cpp:59): holdoff_frames is a public uint8_t; if a
caller sets it to 0, the condition holdoff_counter >= holdoff_frames is
always true (any uint8_t >= 0), causing the AGC outer loop to increase gain
on every non-saturated frame with no holdoff delay. With alternating
sat/no-sat frames this produces a ±step oscillation that prevents the
receiver from settling. Fix: clamp holdoff_frames to a minimum of 1 in the
constructor, preserving all existing test assertions (none use 0; default
remains 4).

Reported-by: shaun0927 (Junghwan) <https://github.com/shaun0927>
2026-04-21 03:35:48 +05:45
Jason
2e9a848908 Merge pull request #117 from NawfalMotii79/fix/agc-gain-arithmetic-overflow
fix(fpga): widen AGC gain arithmetic to 6-bit to prevent wraparound
2026-04-21 00:26:33 +03:00
Jason
3366ac6417 fix(fpga): widen AGC gain arithmetic to 6-bit to prevent wraparound
5-bit signed subtraction in clamp_gain wrapped for agc_attack >= 10 or
agc_decay >= 9 when |agc_gain| + step > 16, inverting gain polarity
instead of clamping — e.g. gain=-7, attack=10 produced +7 (max amplify)
rather than -7 (max attenuate), causing ADC saturation on strong returns.

Widen clamp_gain input to [5:0] and sign-extend both operands to 6 bits
({agc_gain[3],agc_gain[3],agc_gain} and {2'b00,agc_attack/decay}),
covering the full [-22,+22] range before clamping. Default attack/decay
values (1-4) are unaffected; behaviour changes only for values >= 10/9.
2026-04-21 03:06:32 +05:45
Jason
607399ec28 Merge pull request #115 from joyshmitz/fix/live-replay-physical-units-consistency
fix(v7): align live host-DSP units with replay path
2026-04-21 00:01:40 +03:00
Jason
f48448970b fix(v7): wrap long n_doppler fallback line for ruff E501
Line exceeded 100-char limit; wrap with parentheses to stay within
project line-length setting.
2026-04-21 02:40:21 +05:45
Jason
ebd96c90ce fix(v7): store WaveformConfig on self; add set_waveform parity; fix magic 32
- Move WaveformConfig() from per-frame local in _run_host_dsp to
  self._waveform in __init__, mirroring ReplayWorker pattern.
- Add set_waveform() to RadarDataWorker for injection symmetry with
  ReplayWorker.set_waveform() — live path is now configurable.
- Replace hardcoded fallback 32 with self._waveform.n_doppler_bins.
- Update AST contract tests: WaveformConfig() check moves to __init__
  parse; attribute chains updated from ("wf", ...) to
  ("self", "_waveform", ...) to match renamed accessor.
2026-04-21 02:35:53 +05:45
Jason
db80baf34d Merge remote-tracking branch 'origin/main' into develop 2026-04-21 01:33:27 +05:45
NawfalMotii79
33d21da7f2 Remove radar system image from README
Removed the AERIS-10 Radar System image from the README.
2026-04-20 19:04:08 +01:00
NawfalMotii79
18901be04a Fix image link and update mixer model in README
Updated image link and corrected mixer model in specifications.
2026-04-19 19:06:44 +01:00
NawfalMotii79
9f899b96e9 Add files via upload 2026-04-19 19:04:48 +01:00
Serhii
f895c0244c fix(v7): align live host-DSP units with replay path
Use WaveformConfig for live range/velocity conversion in RadarDataWorker
and add headless AST-based regression checks in test_v7.py.

Before: RadarDataWorker._run_host_dsp used RadarSettings.velocity_resolution
(default 1.0 in models.py:113), while ReplayWorker used WaveformConfig
(~5.343 m/s/bin). Live GUI under-reported velocity by factor ~5.34x.

Fix: local WaveformConfig() in _run_host_dsp, mirroring ReplayWorker
pattern. Doppler center derived from frame shape, matching processing.py:520.

Test: TestLiveReplayPhysicalUnitsParity in test_v7.py uses ast.parse on
workers.py (no v7.workers import, headless-CI-safe despite PyQt6 dep)
and asserts AST Call/Attribute/BinOp nodes for both RadarDataWorker
and ReplayWorker paths.
2026-04-19 19:28:03 +03:00
Jason
c82b25f7a0 Merge pull request #113 from NawfalMotii79/fix/adar1000-channel-rotation
fix: ADAR1000 channel indexing + 400 MHz reset fan-out
2026-04-19 14:05:50 +03:00
Jason
2539d46d93 merge: resolve conflicts with develop (supersede by PR #89 / #107)
Three conflicts — all resolved in favor of develop, which has a more
refined version of the same work this branch introduced:

- radar_system_top.v: develop's cleaner USB_MODE=1 comment (same value).
- run_regression.sh: develop's ${SYSTEM_RTL[@]} refactor + added
  USB_MODE=1 test variants.
- tb/radar_system_tb.v: develop's ifdef USB_MODE_1 to dump the correct
  USB instance based on mode.

The 400 MHz reset fan-out fix (nco_400m_enhanced, cic_decimator_4x_enhanced,
ddc_400m) and ADAR1000 channel-indexing fix remain intact on this branch.
2026-04-19 16:28:07 +05:45
NawfalMotii79
88ca1910ec Merge pull request #109 from NawfalMotii79/develop
Release: merge develop into main
2026-04-19 01:27:15 +01:00
Jason
d0b3a4c969 fix(fpga): registered reset fan-out at 400 MHz; default USB to FT2232H
Replace direct !reset_n async sense with a registered active-high reset_h
(max_fanout=50) in nco_400m_enhanced, cic_decimator_4x_enhanced, and
ddc_400m.  The prior single-LUT1 / 700+ load net was the root cause of
WNS=-0.626 ns in the 400 MHz clock domain on the xc7a50t build.  Vivado
replicates the constrained register into ≈14 regional copies, each driving
≤50 loads, closing timing at 2.5 ns.

Change radar_system_top default USB_MODE from 0 (FT601) to 1 (FT2232H).
FT601 remains available for the 200T premium board via explicit parameter
override; the 50T production wrapper already hard-codes USB_MODE=1.

Regression: add usb_data_interface_ft2232h.v to PROD_RTL lint list and
both system-top TB compile commands; fix legacy radar_system_tb hierarchical
probe from gen_ft601.usb_inst to gen_ft2232h.usb_inst.

Golden reference files (rtl_bb_dc.csv, rx_final_doppler_out.csv,
golden_doppler.mem) regenerated to reflect the +1-cycle registered-reset
boundary behaviour; Receiver golden-compare passes 18/18 checks.

All 25 regression tests pass (0 failures, 0 skipped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v2.0.1-reset-fanout
2026-04-18 20:34:52 +05:45
Jason
2f5ddbd8a3 Merge pull request #110 from joyshmitz/docs/contributing-ai-usage-policy
docs(contributing): add AI usage policy section (from #106)
2026-04-18 16:46:30 +03:00
Serhii
aa5d712aea docs(contributing): add AI usage policy section (closes #106 discussion)
Surfaces the three-point AI usage policy Jason articulated in #106 by
placing it in CONTRIBUTING.md between "Code Standards & Tooling" and
"Running the Test Suites", so first-time AI-assisted contributors see
the expectation in the onboarding doc rather than having to discover
it via issue archaeology. Text is the #106 comment verbatim with two
small typo fixes only (use if AI -> use of AI; doesnt -> doesn't); no
structural or stylistic rewriting.

Per Jason's green light to open this PR in
NawfalMotii79/PLFM_RADAR#106 (comment-4273144522).
2026-04-18 10:51:36 +03:00
Jason
475f390a13 docs: rewrite CONTRIBUTING.md with updated workflow and standards 2026-04-18 09:45:34 +05:45
Jason
0731aae2bc docs(readme): update features to list Hybrid AGC 2026-04-18 09:30:17 +05:45
Jason
e62abc9170 fix(readme): point dashboard image to existing GUI_V6.gif 2026-04-18 09:28:26 +05:45
Jason
582476fa0d fix(adar1000): correct 1-based channel indexing in setters (issue #90)
The four channel-indexed ADAR1000 setters (adarSetRxPhase, adarSetTxPhase,
adarSetRxVgaGain, adarSetTxVgaGain) computed their register offset as
`(channel & 0x03) * stride`, which silently aliased CH4 (channel=4 ->
mask=0) onto CH1 and shifted CH1..CH3 by one. The API contract (1-based
CH1..CH4) is documented in ADAR1000_AGC.cpp:76 and matches the ADI
datasheet; every existing caller already passes `ch + 1`.

Fix: subtract 1 before masking -- `((channel - 1) & 0x03) * stride` --
and reject `channel < 1 || channel > 4` early with a DIAG message so a
future stale 0-based caller fails loudly instead of writing to CH4.

Adds TestTier1Adar1000ChannelRegisterRoundTrip (9 tests) which closes
the loop independently of the driver:
  - parses the ADI register map directly from ADAR1000_Manager.h,
  - verifies the datasheet stride invariants (gain=1, phase=2),
  - auto-discovers every C++ TU under MCU_LIB_DIR / MCU_CODE_DIR so a
    new caller cannot silently escape the round-trip check,
  - asserts every caller's channel argument evaluates to {1,2,3,4} for
    ch in {0,1,2,3} (catches bare 0-based or literal-0 callers at CI
    time before the runtime bounds-check would silently drop them),
  - round-trips each (caller, ch) through the helper arithmetic and
    checks the final address equals REG_CH{ch+1}_*.

Adversarially validated: reverting any one helper, all four helpers,
corrupting the parsed register map, injecting a bare-ch caller, and
auto-discovering a literal-0 caller in a fresh TU each cause the
expected (and only the expected) test to fail.

Stacked on fix/adar1000-vm-tables (PR #107).
2026-04-18 06:39:07 +05:45
NawfalMotii79
d3476139e3 Merge pull request #89 from NawfalMotii79/feat/ft2232h-default-ft601-option
feat: make FT2232H default USB interface, add FT601 premium option, deprecate GUI V6
2026-04-17 22:21:58 +01:00
NawfalMotii79
8fac1cc1a0 Merge pull request #107 from NawfalMotii79/fix/adar1000-vm-tables
fix(adar1000): populate VM_I/VM_Q phase tables; remove dead VM_GAIN
2026-04-17 21:58:59 +01:00
Jason
7c91a3e0b9 fix(adar1000): populate VM_I/VM_Q phase tables; remove dead VM_GAIN
The ADAR1000 vector-modulator I/Q lookup tables VM_I[128] and VM_Q[128]
were declared but defined as empty initialiser lists since the first
commit (5fbe97f). Every call to adarSetRxPhase / adarSetTxPhase therefore
wrote (I=0x00, Q=0x00) to registers 0x21/0x23 (Rx) and 0x32/0x34 (Tx)
regardless of the requested phase state, leaving beam steering completely
non-functional in firmware.

This commit:

* Populates VM_I[128] and VM_Q[128] from ADAR1000 datasheet Rev. B
  Tables 13-16 (p.34) on a uniform 2.8125 deg grid (360 / 128 states).
  Byte format: bits[7:6] reserved 0, bit[5] polarity (1 = positive
  lobe), bits[4:0] 5-bit unsigned magnitude - exactly as specified.
* Removes VM_GAIN[128] declaration and (empty) definition. The
  ADAR1000 has no separate VM gain register; per-channel VGA gain is
  set via CHx_RX_GAIN (0x10-0x13) / CHx_TX_GAIN (0x1C-0x1F) by
  adarSetRxVgaGain / adarSetTxVgaGain. VM_GAIN was never populated,
  never read anywhere in the firmware, and its presence falsely
  suggested a missing scaling step in the signal path.
* Adds 9_Firmware/tests/cross_layer/adar1000_vm_reference.py: an
  independently-derived ground-truth module containing the full
  datasheet table plus byte-format / uniform-grid / quadrant-symmetry
  / cardinal-point invariant checkers and a tolerant C array parser.
* Adds TestTier2Adar1000VmTableGroundTruth (9 tests) to
  test_cross_layer_contract.py, including a tokenising C/C++
  comment+string stripper used by the VM_GAIN reintroduction guard,
  and an adversarial self-test that corrupts one byte and asserts
  the comparison detects it (defends against silent bypass via
  future fixture/parser refactors).

Adversarially validated: removing the firmware definitions, flipping
a single byte, or reintroducing VM_GAIN as code each cause the suite
to fail; restoring causes it to pass. VM_GAIN appearing inside string
literals or comments correctly does NOT trip the guard.

Closes the empty-table half of the ADAR1000 phase-control bug class.
The separate channel-rotation issue (#90) will be addressed in a
follow-up PR.

Refs: 7_Components Datasheets and Application notes/ADAR1000.pdf
      Rev. B Tables 13-16 p.34
2026-04-18 02:02:07 +05:45
Serhii
d2e2693c2f test(cross-layer): enforce status word field positions match Verilog concat layout
Tier-1 had only one explicit per-field assertion (radar_mode at lsb=22).
The Tier-2 round-trip uses the same Python parser as oracle for status
word decoding, so a coupled Verilog+Python bit-position shift in
status_words[0]/[3]/[4]/[5] passes Tier-2 silently — only [1] and [2]
have an independent raw-byte check in tb_cross_layer_ft2232h.v.

Add an independent static check that walks each Verilog status_words[N]
concatenation MSB->LSB via count_concat_bits, computes (word_idx, lsb,
width) for every named payload fragment, drops literal padding, and
compares against every field parse_status_packet() extracts. Name match
is status_<python_name> (current convention has zero exceptions).
Mismatches accumulate into a single failure message so one run surfaces
all drift at once.

Parametrized over both usb_data_interface_ft2232h.v and
usb_data_interface.v since both are live post PR #89.
2026-04-17 20:48:25 +03:00
Jason
fd6cff5b2b Merge pull request #102 from JJassonn69/chore/sync-main-into-develop
chore: sync main → develop (schematic updates + README + project doc)
2026-04-17 19:31:31 +03:00
Jason
964f1903f3 chore: sync main → develop (schematic updates, README, project doc)
Brings in main-only commits that never reached develop:
  754d919 Added silk screen and headers description (MainBoard .brd)
  0443516 Added thermal vias (RF_PA .brd)
  5fbe051 Added ABAC INDUSTRY web site (Project_Description.docx)
  12b549d / 5d5e9ff Merge PR #101: README BOM sensor counts fix

No conflicts expected: develop has not touched any of these paths.
2026-04-17 22:12:26 +05:45
Jason
12b549dafb Merge pull request #101 from JJassonn69/fix/readme-bom-sensor-counts
docs(readme): correct STM32 peripheral counts and locations to match production BOM
2026-04-17 17:21:59 +03:00
Jason
5d5e9ff297 docs(readme): correct BOM sensor counts and locations
The STM32 peripheral list in the README disagreed with the production
BOM (4_7_Production Files/Gerber_Main_Board/RADAR_Main_Board_BOM_csv)
and with the firmware (9_1_Microcontroller/.../main.cpp). Corrections
based on origin/main commit 754d919:

- ADS7830 Idq ADCs: placed on the Main Board (U88 @ 0x48, U89 @ 0x4A),
  not on the Power Amplifier Boards. Added the INA241A3 (x50) and 5 mOhm
  shunt detail that completes the current-sense chain.
- DAC5578 Vg DACs: placed on the Main Board (U7 @ 0x48, U69 @ 0x49),
  not on the Power Amplifier Boards. Noted closed-loop Idq calibration
  at boot (main.cpp powerUpSequence).
- Temperature sensors: 1x ADS7830 (U10) with 8 single-ended channels
  reading 8 thermistors -- not 8 separate ADS7830 chips. Cooling is a
  single GPIO (EN_DIS_COOLING), bang-bang, not PWM.
- GPS: reflect the UM982 driver merged in #79 and its role in
  per-detection position tagging beyond map centering.

Counts now match the 3x ADS7830 / 2x DAC5578 / 16x INA241A3 population
in the production BOM.
2026-04-17 20:04:01 +05:45
NawfalMotii79
754d919e44 Added silk screen and headers description 2026-04-16 23:48:23 +01:00
NawfalMotii79
0443516cc9 Added thermal vias 2026-04-16 23:47:24 +01:00
NawfalMotii79
5fbe0513b5 Added ABAC INDUSTRY web site 2026-04-16 23:46:08 +01:00
Jason
c3db8a9122 Merge pull request #96 from joyshmitz/chore/remove-dead-adar1000-c-api
chore(mcu): remove dead C-style adar1000 driver
2026-04-16 23:51:22 +03:00
Jason
ec8256e25a Merge pull request #95 from joyshmitz/test/agc-debounce-enforce
test(cross-layer): enforce 2-frame DIG_6 debounce guard on outerAgc.enabled (follow-up to #93)
2026-04-16 23:42:12 +03:00