Patcher: disclaimer, every mod can be unticked, hot cue docs
- A disclaimer (mods and patcher developed with Claude Code, at your own risk) must be accepted by typing yes, or with --accept, before a build. - Unticking a mod unticks what needs it instead of ticking it back, so code can be unticked; hotcuekeys needs cuecolours. - beatjump starts unticked and is marked with its faults on a player. - README: the hot cue keys (play, gate, set, delete, LEDs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cfa0e85854
commit
bbe784aad2
3 files changed
+119
-16
No files matched your search
@@ -5,13 +5,20 @@ the mods you choose, for the player's own USB update.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
python cdj900nxs_patcher.py C900NXS.UPD # choose in a list
|
python cdj900nxs_patcher.py C900NXS.UPD # choose in a list
|
||||||
python cdj900nxs_patcher.py C900NXS.UPD --mods beatjump,cuecolours,hotcuekeys
|
python cdj900nxs_patcher.py C900NXS.UPD --mods cuecolours,hotcuekeys
|
||||||
python cdj900nxs_patcher.py C900NXS.UPD --stock # the original, to go back
|
python cdj900nxs_patcher.py C900NXS.UPD --stock # the original, to go back
|
||||||
python cdj900nxs_patcher.py --list
|
python cdj900nxs_patcher.py --list
|
||||||
```
|
```
|
||||||
|
|
||||||
In the list: **arrow keys** move, **space** ticks or unticks, **a** ticks all
|
In the list: **arrow keys** move, **space** ticks or unticks, **a** ticks all
|
||||||
or none, **enter** builds, **q** quits. Terminals that cannot pass single
|
or none, **enter** builds, **q** quits. Ticking a mod ticks what it needs;
|
||||||
|
unticking one unticks what needs it (`utility` needs `code`, `hotcuekeys`
|
||||||
|
needs `cuecolours`). A mod with known faults starts unticked.
|
||||||
|
|
||||||
|
Before anything is built the patcher shows a disclaimer that must be
|
||||||
|
accepted by typing `yes` (or with `--accept`, where no one can type): the
|
||||||
|
mods and the patcher were developed with Claude Code, Anthropic's AI coding
|
||||||
|
assistant, and are used at your own risk. Terminals that cannot pass single
|
||||||
keys (Git Bash's mintty, a pipe) get a numbered list instead. The result goes
|
keys (Git Bash's mintty, a pipe) get a numbered list instead. The result goes
|
||||||
to `out/<date>-<mods>/C900NXS.UPD` with a `report.txt` of every check.
|
to `out/<date>-<mods>/C900NXS.UPD` with a `report.txt` of every check.
|
||||||
|
|
||||||
@@ -50,9 +57,23 @@ From the emulator checkout `../CDJ900NXSVirtual` (`--repo` for another place):
|
|||||||
| `code` | arnoldasr: BPM with two decimals, two-line phase meter, RGB waveform |
|
| `code` | arnoldasr: BPM with two decimals, two-line phase meter, RGB waveform |
|
||||||
| `utility` | arnoldasr: UTILITY > PHASE METER switch (needs `code`) |
|
| `utility` | arnoldasr: UTILITY > PHASE METER switch (needs `code`) |
|
||||||
| `overview` | arnoldasr: overview waveform colour gradient |
|
| `overview` | arnoldasr: overview waveform colour gradient |
|
||||||
| `beatjump` | TRACK SEARCH jumps 1/2/4/8 beats, UTILITY > BEAT JUMP |
|
| `beatjump` | TRACK SEARCH jumps 1/2/4/8 beats, UTILITY > BEAT JUMP (known faults on a player, see Risk) |
|
||||||
| `cuecolours` | memory cues in their rekordbox colours, hot cues shown |
|
| `cuecolours` | memory cues in their rekordbox colours, hot cues shown |
|
||||||
| `hotcuekeys` | UTILITY > HOT CUE KEYS; QUANTIZE short = hot key mode |
|
| `hotcuekeys` | hot cues A-E on the BEAT DIVIDE keys (needs `cuecolours`), see below |
|
||||||
|
|
||||||
|
`hotcuekeys`: UTILITY > HOT CUE KEYS ON; then QUANTIZE pressed briefly
|
||||||
|
switches the hot key mode (HOT KEYS box beside QUANTIZE), held it is
|
||||||
|
QUANTIZE. In the mode the BEAT DIVIDE keys 3/4, 1/3, 1/2, 1/4, 1/8 are hot
|
||||||
|
cues A-E:
|
||||||
|
|
||||||
|
- press: jump to the hot cue and play (from pause too);
|
||||||
|
- hold 0.5 s or longer, then let go: back to the hot cue, standing (gate);
|
||||||
|
- an empty key held 0.8 s while paused: a hot cue there;
|
||||||
|
- DELETE held, a key pressed: its hot cue deleted (DELETE alone is DELETE);
|
||||||
|
- LEDs: off without a hot cue, dim with one, two bright blinks when pressed.
|
||||||
|
|
||||||
|
Hot cues set or deleted live until the next track load; nothing is written
|
||||||
|
to the USB stick. As with CALL, the CUE point moves to the hot cue.
|
||||||
|
|
||||||
arnoldasr's mods need his repository checked out in
|
arnoldasr's mods need his repository checked out in
|
||||||
`../CDJ900NXSVirtual/mods/cdj900nxs/upstream` (it has no licence, so it is
|
`../CDJ900NXSVirtual/mods/cdj900nxs/upstream` (it has no licence, so it is
|
||||||
@@ -61,7 +82,10 @@ not copied here); without it they are greyed out.
|
|||||||
## Risk
|
## Risk
|
||||||
|
|
||||||
Nothing here talks to a player; you copy the file to a FAT32 USB stick and run
|
Nothing here talks to a player; you copy the file to a FAT32 USB stick and run
|
||||||
the update yourself. Our mods have only run in the emulator. Recovery from an
|
the update yourself. Our mods were developed with Claude Code in an emulator.
|
||||||
|
On a player: `beatjump` jumped off the grid and, after a few jumps, reported a
|
||||||
|
corrupted file and needed a restart; it is being reworked. `cuecolours` and
|
||||||
|
earlier versions of `hotcuekeys` ran on a player. Recovery from an
|
||||||
application that crashes at start-up has not been tested by anyone. Keep the
|
application that crashes at start-up has not been tested by anyone. Keep the
|
||||||
`--stock` file at hand.
|
`--stock` file at hand.
|
||||||
|
|
||||||
|
|||||||
+58
-11
@@ -16,6 +16,9 @@ choose, as a new C900NXS.UPD for the player's own USB update.
|
|||||||
* The new file says 1.32 in its MAIN header so that the player's updater
|
* The new file says 1.32 in its MAIN header so that the player's updater
|
||||||
does not skip it as the version it already has; the application itself
|
does not skip it as the version it already has; the application itself
|
||||||
still reports 1.31 (as arnoldasr's builds do). Only that label differs.
|
still reports 1.31 (as arnoldasr's builds do). Only that label differs.
|
||||||
|
* Before anything is built a disclaimer must be accepted: the mods and
|
||||||
|
this patcher were developed with Claude Code (--accept where no one can
|
||||||
|
type).
|
||||||
* Nothing here talks to a player. You copy the file to a USB stick and run
|
* Nothing here talks to a player. You copy the file to a USB stick and run
|
||||||
the update on the player yourself, at your own risk: nothing of this has
|
the update on the player yourself, at your own risk: nothing of this has
|
||||||
been tested on hardware except the relabelled stock update (by arnoldasr),
|
been tested on hardware except the relabelled stock update (by arnoldasr),
|
||||||
@@ -36,8 +39,11 @@ import upd # noqa: E402
|
|||||||
|
|
||||||
DEFAULT_REPO = os.path.join(os.path.dirname(HERE), 'CDJ900NXSVirtual')
|
DEFAULT_REPO = os.path.join(os.path.dirname(HERE), 'CDJ900NXSVirtual')
|
||||||
UPSTREAM_MODS = ('code', 'utility', 'overview')
|
UPSTREAM_MODS = ('code', 'utility', 'overview')
|
||||||
NEEDS = {'utility': 'code'} # the PHASE METER switch chooses the code mod's meter
|
NEEDS = {'utility': 'code', # the PHASE METER switch chooses the code mod's meter
|
||||||
|
'hotcuekeys': 'cuecolours'} # the hot cues the keys play are read by cuecolours
|
||||||
LABEL = '1.32'
|
LABEL = '1.32'
|
||||||
|
# not ticked at the start of the list, and why
|
||||||
|
KNOWN_FAULTS = {'beatjump': 'known faults on a player (jumps off the grid, "file corrupted" after a few jumps)'}
|
||||||
|
|
||||||
|
|
||||||
def load_repo(repo):
|
def load_repo(repo):
|
||||||
@@ -60,6 +66,8 @@ def available(patcher, repo):
|
|||||||
or os.path.join(repo, 'mods', 'cdj900nxs', 'upstream'))
|
or os.path.join(repo, 'mods', 'cdj900nxs', 'upstream'))
|
||||||
out = []
|
out = []
|
||||||
for name, what in patcher.MODS.items():
|
for name, what in patcher.MODS.items():
|
||||||
|
if name in KNOWN_FAULTS:
|
||||||
|
what = '%s [%s]' % (what, KNOWN_FAULTS[name])
|
||||||
ok = upstream or name not in UPSTREAM_MODS
|
ok = upstream or name not in UPSTREAM_MODS
|
||||||
out.append((name, what, ok, '' if ok else "needs arnoldasr's checkout in mods/cdj900nxs/upstream"))
|
out.append((name, what, ok, '' if ok else "needs arnoldasr's checkout in mods/cdj900nxs/upstream"))
|
||||||
return out
|
return out
|
||||||
@@ -102,9 +110,16 @@ def _keys():
|
|||||||
return read
|
return read
|
||||||
|
|
||||||
|
|
||||||
def _fix_needs(chosen, mods):
|
def _toggle(chosen, name, mods):
|
||||||
for name, need in NEEDS.items():
|
"""Tick or untick one mod: ticking one ticks what it needs, unticking
|
||||||
if name in chosen and need not in chosen and any(m[0] == need and m[2] for m in mods):
|
one unticks what needs it (so every mod can be unticked)."""
|
||||||
|
if name in chosen:
|
||||||
|
chosen.discard(name)
|
||||||
|
chosen.difference_update(n for n, need in NEEDS.items() if need == name)
|
||||||
|
else:
|
||||||
|
chosen.add(name)
|
||||||
|
need = NEEDS.get(name)
|
||||||
|
if need and any(m[0] == need and m[2] for m in mods):
|
||||||
chosen.add(need)
|
chosen.add(need)
|
||||||
|
|
||||||
|
|
||||||
@@ -115,7 +130,7 @@ def choose(mods):
|
|||||||
return choose_by_number(mods)
|
return choose_by_number(mods)
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
os.system('') # VT sequences on in the Windows console
|
os.system('') # VT sequences on in the Windows console
|
||||||
chosen = {m[0] for m in mods if m[2]}
|
chosen = {m[0] for m in mods if m[2] and m[0] not in KNOWN_FAULTS}
|
||||||
at = 0
|
at = 0
|
||||||
lines = len(mods) + 3
|
lines = len(mods) + 3
|
||||||
|
|
||||||
@@ -140,8 +155,7 @@ def choose(mods):
|
|||||||
elif k == 'down':
|
elif k == 'down':
|
||||||
at = (at + 1) % len(mods)
|
at = (at + 1) % len(mods)
|
||||||
elif k == 'space' and mods[at][2]:
|
elif k == 'space' and mods[at][2]:
|
||||||
chosen ^= {mods[at][0]}
|
_toggle(chosen, mods[at][0], mods)
|
||||||
_fix_needs(chosen, mods)
|
|
||||||
elif k == 'all':
|
elif k == 'all':
|
||||||
usable = {m[0] for m in mods if m[2]}
|
usable = {m[0] for m in mods if m[2]}
|
||||||
chosen = set() if chosen == usable else usable
|
chosen = set() if chosen == usable else usable
|
||||||
@@ -155,9 +169,9 @@ def choose(mods):
|
|||||||
|
|
||||||
def choose_by_number(mods):
|
def choose_by_number(mods):
|
||||||
"""For terminals that cannot be read key by key (mintty, pipes)."""
|
"""For terminals that cannot be read key by key (mintty, pipes)."""
|
||||||
chosen = {m[0] for m in mods if m[2]}
|
chosen = {m[0] for m in mods if m[2] and m[0] not in KNOWN_FAULTS}
|
||||||
while True:
|
while True:
|
||||||
print('\n Mods (all usable ones are ticked):')
|
print('\n Mods (all usable ones without known faults are ticked):')
|
||||||
for i, (name, what, ok, why) in enumerate(mods, 1):
|
for i, (name, what, ok, why) in enumerate(mods, 1):
|
||||||
mark = '[x]' if name in chosen else ('[ ]' if ok else ' - ')
|
mark = '[x]' if name in chosen else ('[ ]' if ok else ' - ')
|
||||||
print(' %d %s %-11s %s' % (i, mark, name, what if ok else why))
|
print(' %d %s %-11s %s' % (i, mark, name, what if ok else why))
|
||||||
@@ -168,11 +182,41 @@ def choose_by_number(mods):
|
|||||||
raise SystemExit('nothing written')
|
raise SystemExit('nothing written')
|
||||||
for tok in answer.replace(',', ' ').split():
|
for tok in answer.replace(',', ' ').split():
|
||||||
if tok.isdigit() and 1 <= int(tok) <= len(mods) and mods[int(tok) - 1][2]:
|
if tok.isdigit() and 1 <= int(tok) <= len(mods) and mods[int(tok) - 1][2]:
|
||||||
chosen ^= {mods[int(tok) - 1][0]}
|
_toggle(chosen, mods[int(tok) - 1][0], mods)
|
||||||
_fix_needs(chosen, mods)
|
|
||||||
return [m[0] for m in mods if m[0] in chosen]
|
return [m[0] for m in mods if m[0] in chosen]
|
||||||
|
|
||||||
|
|
||||||
|
# --- the disclaimer ------------------------------------------------------------
|
||||||
|
|
||||||
|
DISCLAIMER = '''
|
||||||
|
Please read before anything is built:
|
||||||
|
|
||||||
|
The mods of this patcher (beatjump, cuecolours, hotcuekeys) and the patcher
|
||||||
|
itself were developed with Claude Code, Anthropic's AI coding assistant,
|
||||||
|
by reverse engineering the firmware in an emulator. They have not been
|
||||||
|
reviewed by Pioneer DJ / AlphaTheta and have not been tested on a player
|
||||||
|
except where the README says so. Flashing modified firmware can leave the
|
||||||
|
player unusable. You use the result at your own risk; there is no warranty.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def confirm(accepted, ask=input):
|
||||||
|
"""The disclaimer, which must be accepted before a build: by typing yes,
|
||||||
|
or with --accept where no one can type."""
|
||||||
|
print(DISCLAIMER)
|
||||||
|
if accepted:
|
||||||
|
print(' accepted with --accept')
|
||||||
|
return
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
raise SystemExit('nothing written: accept the disclaimer by typing yes, or with --accept')
|
||||||
|
try:
|
||||||
|
answer = ask(' Type yes to accept and go on: ').strip().lower()
|
||||||
|
except EOFError:
|
||||||
|
answer = ''
|
||||||
|
if answer not in ('yes', 'y', 'ja', 'j'):
|
||||||
|
raise SystemExit('nothing written: the disclaimer was not accepted')
|
||||||
|
|
||||||
|
|
||||||
# --- building ------------------------------------------------------------------
|
# --- building ------------------------------------------------------------------
|
||||||
|
|
||||||
def build(stock, names, patcher, lzss, label=LABEL, log=print):
|
def build(stock, names, patcher, lzss, label=LABEL, log=print):
|
||||||
@@ -205,6 +249,8 @@ def main(argv=None):
|
|||||||
help='no mods: the original application labelled %s, to go back to stock' % LABEL)
|
help='no mods: the original application labelled %s, to go back to stock' % LABEL)
|
||||||
ap.add_argument('--repo', default=DEFAULT_REPO, help='the CDJ900NXSVirtual checkout (default %(default)s)')
|
ap.add_argument('--repo', default=DEFAULT_REPO, help='the CDJ900NXSVirtual checkout (default %(default)s)')
|
||||||
ap.add_argument('--list', action='store_true', help='list the mods and exit')
|
ap.add_argument('--list', action='store_true', help='list the mods and exit')
|
||||||
|
ap.add_argument('--accept', action='store_true',
|
||||||
|
help='accept the disclaimer (developed with Claude Code, at your own risk) without asking')
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
patcher, lzss = load_repo(args.repo)
|
patcher, lzss = load_repo(args.repo)
|
||||||
@@ -246,6 +292,7 @@ def main(argv=None):
|
|||||||
print(' no mod chosen: use --stock for the unmodified update')
|
print(' no mod chosen: use --stock for the unmodified update')
|
||||||
return 1
|
return 1
|
||||||
print(' mods: %s' % (', '.join(names) or 'none (stock application)'))
|
print(' mods: %s' % (', '.join(names) or 'none (stock application)'))
|
||||||
|
confirm(args.accept)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
out_raw, app, checks = build(stock, names, patcher, lzss)
|
out_raw, app, checks = build(stock, names, patcher, lzss)
|
||||||
|
|||||||
@@ -93,3 +93,35 @@ def test_boot_area_tampering_caught():
|
|||||||
bad = upd.join([sections[0].blob, upd.encode_section(main.header, recs), sections[2].blob])
|
bad = upd.join([sections[0].blob, upd.encode_section(main.header, recs), sections[2].blob])
|
||||||
with pytest.raises(upd.UpdError, match='boot loader'):
|
with pytest.raises(upd.UpdError, match='boot loader'):
|
||||||
upd.verify(bad, stock, stock.application)
|
upd.verify(bad, stock, stock.application)
|
||||||
|
|
||||||
|
|
||||||
|
MODS = [('code', '', True, ''), ('utility', '', True, ''), ('overview', '', True, ''),
|
||||||
|
('beatjump', '', True, ''), ('cuecolours', '', True, ''), ('hotcuekeys', '', True, '')]
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_mod_can_be_unticked():
|
||||||
|
chosen = {m[0] for m in MODS}
|
||||||
|
cli._toggle(chosen, 'code', MODS)
|
||||||
|
assert 'code' not in chosen and 'utility' not in chosen # what needs it goes with it
|
||||||
|
cli._toggle(chosen, 'cuecolours', MODS)
|
||||||
|
assert 'cuecolours' not in chosen and 'hotcuekeys' not in chosen
|
||||||
|
cli._toggle(chosen, 'hotcuekeys', MODS)
|
||||||
|
assert {'hotcuekeys', 'cuecolours'} <= chosen # and ticking brings what it needs
|
||||||
|
cli._toggle(chosen, 'overview', MODS)
|
||||||
|
assert 'overview' not in chosen
|
||||||
|
|
||||||
|
|
||||||
|
def test_disclaimer_must_be_accepted(monkeypatch):
|
||||||
|
monkeypatch.setattr(cli.sys.stdin, 'isatty', lambda: True, raising=False)
|
||||||
|
assert 'Claude Code' in cli.DISCLAIMER
|
||||||
|
cli.confirm(False, ask=lambda _: 'yes')
|
||||||
|
cli.confirm(True, ask=lambda _: pytest.fail('asked although accepted'))
|
||||||
|
for answer in ('', 'no', 'q'):
|
||||||
|
with pytest.raises(SystemExit, match='not accepted'):
|
||||||
|
cli.confirm(False, ask=lambda _: answer)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disclaimer_without_a_terminal(monkeypatch):
|
||||||
|
monkeypatch.setattr(cli.sys.stdin, 'isatty', lambda: False, raising=False)
|
||||||
|
with pytest.raises(SystemExit, match='--accept'):
|
||||||
|
cli.confirm(False)
|
||||||
Reference in new issue
Block a user