feat: import hydra-sc + portable ~base
Context: while inspecting the MBP we found ~/hydra-sc/, an earlier
prototype the user had built before AV-Live existed. It contained
working pieces we needed (Hantek firmware loader, OSC scope bridges,
SC↔Hydra ws relay, hydra patch). We also discovered that 00_load.scd
hard-coded ~base to the GrosMac dev path, which silently broke the
whole sclang engine on the MBP — every (~base ++ '...').load was
hitting a non-existent path and quietly failing. The user's existing
control/midi.scd is comprehensive but split into 5 IDE-friendly
top-level blocks, so it isn't .load-compatible and never auto-runs at
boot.
Approach: derive ~base from thisProcess.nowExecutingPath at load time
so the same project tree works from any absolute location. Migrate the
hydra-sc artifacts into AV-Live/{tools,sound_algo/control,
sound_algo/web/public/hydra} so everything lives in one repo. Add a
single-block live/midi_init.scd that does MIDIClient.init +
MIDIIn.connectAll + the default mapping in one .load-friendly chunk,
deferred 0.5s through AppClock so MIDI device discovery doesn't block
the main load chain. Old ~/hydra-sc/ on the MBP deleted, the orphan
hydra-sc-bridge node process (PID 41421 from a previous session)
killed.
Changes:
- sound_algo/00_load.scd : both ~base assignments now use
thisProcess.nowExecutingPath.dirname with fallback to the legacy
hard-coded path
- tools/hantek/{fx2_fwload,hantek_to_osc,hantek_osc_bridge}.py :
imported as-is, plus a new README explaining the firmware-load /
C++-or-sigrok choice and the macOS prerequisites (PulseView,
sigrok-cli, python-osc)
- sound_algo/control/hantek_receiver.scd : OSCdef \hantek that
exposes ~hPk1/~hRms1/~hPk2/~hRms2 from the Python bridge — kept as
alternate path to oscope-of's C++ driver
- sound_algo/control/hydra_send.scd : SC→Hydra OSC sender (was
sc_to_hydra.scd in hydra-sc) — analyzes SoundIn.ar(0) into rms /
centroid / flatness and sends to 127.0.0.1:8000
- sound_algo/web/public/hydra/external_patch.js : Hydra patch the
user pastes into hydra.ojack.xyz when running the external bridge
- sound_algo/live/midi_init.scd : single-block MIDI auto-init with
the same CC/note bindings as control/midi.scd plus new note
ranges for albums (notes 36-59 → ~playAlbum A-X), kicks
(notes 60-71 → ~kkAt index 0..11), melodies (72-83 → ~mmAt),
fx (84-95 → ~ffAt), CC 7 → master volume. Idempotent (frees
previous MIDIFunc on re-load), graceful when no MIDI device is
detected (skips mapping, prints note)
Impact: sclang now actually boots its engine on the MBP (~base path
was the silent killer). MIDI controllers connected at startup get
the default mapping automatically. The hydra-sc legacy is fully
absorbed into AV-Live so there's one place to find everything.
Note: live/midi_init.scd is NOT yet wired into live/_load.scd's
auto-load chain — that's a follow-up so we can verify the file
loads cleanly first.
This commit is contained in:
+18
-3
@@ -88,7 +88,12 @@
|
||||
// Utile si tu veux juste set le chemin avant de Cmd+Entree sur un
|
||||
// autre bloc. CHARGER TOUT et CHARGER MINIMAL le font deja.
|
||||
(
|
||||
~base = "/Users/electron/Documents/Projets_Creatifs/sound_algo-refonte-v2/";
|
||||
// Auto-detect ~base from this file's location so the project is portable
|
||||
// across machines (GrosMac dev path vs MBP /Users/electron/AV-Live/sound_algo/
|
||||
// vs anywhere else). Falls back to the legacy hard-coded path only if the
|
||||
// file isn't being executed from disk (rare — Cmd+Enter on a buffer).
|
||||
~base = (thisProcess.nowExecutingPath !? { |p| p.dirname ++ "/" })
|
||||
?? "/Users/electron/Documents/Projets_Creatifs/sound_algo-refonte-v2/";
|
||||
"~base = ".post; ~base.postln;
|
||||
)
|
||||
|
||||
@@ -100,7 +105,12 @@
|
||||
//
|
||||
// Ce bloc est AUTONOME : pas besoin d'evaluer BASE PATH avant.
|
||||
(
|
||||
~base = "/Users/electron/Documents/Projets_Creatifs/sound_algo-refonte-v2/";
|
||||
// Auto-detect ~base from this file's location so the project is portable
|
||||
// across machines (GrosMac dev path vs MBP /Users/electron/AV-Live/sound_algo/
|
||||
// vs anywhere else). Falls back to the legacy hard-coded path only if the
|
||||
// file isn't being executed from disk (rare — Cmd+Enter on a buffer).
|
||||
~base = (thisProcess.nowExecutingPath !? { |p| p.dirname ++ "/" })
|
||||
?? "/Users/electron/Documents/Projets_Creatifs/sound_algo-refonte-v2/";
|
||||
(~base ++ "engine.scd").load;
|
||||
(~base ++ "synth/_index.scd").load;
|
||||
(~base ++ "fx/_index.scd").load;
|
||||
@@ -121,7 +131,12 @@
|
||||
// Pour debugger ou demarrer sans live/mixer/fx. Pas de boot serveur
|
||||
// automatique, pas de helpers ~check / ~setupAll.
|
||||
(
|
||||
~base = "/Users/electron/Documents/Projets_Creatifs/sound_algo-refonte-v2/";
|
||||
// Auto-detect ~base from this file's location so the project is portable
|
||||
// across machines (GrosMac dev path vs MBP /Users/electron/AV-Live/sound_algo/
|
||||
// vs anywhere else). Falls back to the legacy hard-coded path only if the
|
||||
// file isn't being executed from disk (rare — Cmd+Enter on a buffer).
|
||||
~base = (thisProcess.nowExecutingPath !? { |p| p.dirname ++ "/" })
|
||||
?? "/Users/electron/Documents/Projets_Creatifs/sound_algo-refonte-v2/";
|
||||
(~base ++ "engine.scd").load;
|
||||
(~base ++ "synth/_index.scd").load;
|
||||
(~base ++ "fx/_index.scd").load;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Réception du pont Hantek → OSC → SC
|
||||
// Lance d'abord ./hantek_to_osc.py dans un terminal
|
||||
//
|
||||
// Variables globales mises à jour à ~50 Hz :
|
||||
// ~hPk1, ~hRms1 (CH1 peak / RMS, 0..1)
|
||||
// ~hPk2, ~hRms2 (CH2 peak / RMS, 0..1)
|
||||
|
||||
(
|
||||
~hPk1 = 0; ~hRms1 = 0; ~hPk2 = 0; ~hRms2 = 0;
|
||||
|
||||
OSCdef(\hantek, { |msg|
|
||||
~hPk1 = msg[1]; ~hRms1 = msg[2];
|
||||
~hPk2 = msg[3]; ~hRms2 = msg[4];
|
||||
}, '/scope');
|
||||
|
||||
// Démo : un sinus dont la fréquence suit CH1, l'amplitude suit CH2
|
||||
SynthDef(\hantekVoice, {
|
||||
Out.ar(0, SinOsc.ar(\freq.kr(220) * 2, 0, \amp.kr(0.1)).dup);
|
||||
}).add;
|
||||
|
||||
s.waitForBoot {
|
||||
~v = Synth(\hantekVoice);
|
||||
~poll = Routine({
|
||||
loop {
|
||||
~v.set(
|
||||
\freq, ~hPk1.linlin(0, 1, 80, 1200),
|
||||
\amp, ~hRms2.linlin(0, 1, 0, 0.4)
|
||||
);
|
||||
0.02.wait;
|
||||
}
|
||||
}).play;
|
||||
};
|
||||
|
||||
// Stop: ~poll.stop; ~v.free;
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
// SC → Hydra OSC bridge demo
|
||||
// 1. Lance le bridge dans un terminal: hydra-sc-bridge
|
||||
// 2. Ouvre https://hydra.ojack.xyz/ et colle hydra_patch.js
|
||||
// 3. Évalue ce code dans scide
|
||||
|
||||
(
|
||||
s.waitForBoot {
|
||||
SynthDef(\hydraSend, {
|
||||
var in, fft, rms, centroid, flatness;
|
||||
in = SoundIn.ar(0) + WhiteNoise.ar(0.1);
|
||||
fft = FFT(LocalBuf(2048), in);
|
||||
rms = Amplitude.kr(in, 0.01, 0.3);
|
||||
centroid = SpecCentroid.kr(fft).cpsmidi.linlin(20, 127, 0, 1);
|
||||
flatness = SpecFlatness.kr(fft);
|
||||
SendReply.kr(Impulse.kr(30), '/hydra', [rms, centroid, flatness]);
|
||||
Out.ar(0, in.dup);
|
||||
}).add;
|
||||
|
||||
s.sync;
|
||||
|
||||
// Forward SendReply → OSC bridge (default port 8000)
|
||||
OSCdef(\hydraFwd, { |msg|
|
||||
NetAddr("127.0.0.1", 8000).sendMsg("/rms", msg[3], msg[4], msg[5]);
|
||||
}, '/hydra');
|
||||
|
||||
Synth(\hydraSend);
|
||||
"=> SC → Hydra bridge actif".postln;
|
||||
};
|
||||
)
|
||||
@@ -0,0 +1,174 @@
|
||||
// =====================================================================
|
||||
// live/midi_init.scd -- Auto-init MIDI au boot
|
||||
//
|
||||
// Charge MIDIClient + MIDIIn.connectAll + mapping CC/note par defaut
|
||||
// en UN SEUL bloc top-level (.load-compatible, contrairement a
|
||||
// control/midi.scd qui est decoupe pour l'IDE).
|
||||
//
|
||||
// IDEMPOTENT : libere les MIDIFunc precedentes avant de remapper.
|
||||
//
|
||||
// Mapping :
|
||||
// Notes 36-47 (C2-B2) : albums A-L -> ~playAlbum
|
||||
// Notes 48-59 (C3-B3) : albums M-W -> ~playAlbum (12 albums M..X)
|
||||
// Notes 60-71 (C4-B4) : kicks index 0..11 -> ~kkAt
|
||||
// Notes 72-83 (C5-B5) : melodies index 0..11 -> ~mmAt
|
||||
// Notes 84-95 (C6-B6) : fx index 0..11 -> ~ffAt
|
||||
// CC 13-20 : volumes par voix (kick/hat/snare/clap/...)
|
||||
// CC 29-36 : cutoffs / drive / bpm
|
||||
// CC 49-56 : wobble / sidechain / FX
|
||||
// CC 7 (master) : master volume -> ~masterVol
|
||||
//
|
||||
// Customisation : surcharge ~midiMapDefault avant le boot, ou re-map
|
||||
// apres avec control/midi.scd (Cmd+Entree dans l'IDE).
|
||||
// =====================================================================
|
||||
(
|
||||
~midiAutoInit !? { ~midiAutoInit.value }; // teardown precedent
|
||||
|
||||
~midiSafe = { |func, label = "midi"|
|
||||
{ func.value }.try { |err|
|
||||
("[" ++ label ++ "] error: " ++ err.errorString).postln;
|
||||
err.dumpBackTrace;
|
||||
};
|
||||
};
|
||||
|
||||
~midiAutoInit = {
|
||||
"[midi_init] freeing previous MIDIFunc...".postln;
|
||||
~midiFuncs !? { ~midiFuncs.do { |f| f.free } };
|
||||
~midiFuncs = nil;
|
||||
~midiInitialized = false;
|
||||
};
|
||||
|
||||
// --- Sortie ordonnee par index (helpers d'acces aux dicts existants) ---
|
||||
~kkAt = ~kkAt ?? { |i|
|
||||
~kkBank !? {
|
||||
var keys = ~kkBank.keys.asArray.sort;
|
||||
i = i.clip(0, keys.size - 1);
|
||||
~kk.(keys[i]);
|
||||
};
|
||||
};
|
||||
~mmAt = ~mmAt ?? { |i|
|
||||
~mmBank !? {
|
||||
var keys = ~mmBank.keys.asArray.sort;
|
||||
i = i.clip(0, keys.size - 1);
|
||||
~mm.(keys[i]);
|
||||
};
|
||||
};
|
||||
~ffAt = ~ffAt ?? { |i|
|
||||
~ffBank !? {
|
||||
var keys = ~ffBank.keys.asArray.sort;
|
||||
i = i.clip(0, keys.size - 1);
|
||||
~ff.(keys[i]);
|
||||
};
|
||||
};
|
||||
|
||||
~midiMapDefault = {
|
||||
~midiFuncs = List.new;
|
||||
|
||||
// Debounce ~reload (evite spam)
|
||||
~reloadPending = ~reloadPending ?? false;
|
||||
~debouncedReload = {
|
||||
if (~reloadPending.not) {
|
||||
~reloadPending = true;
|
||||
AppClock.sched(0.05, { ~reloadPending = false; ~reload.value; nil });
|
||||
};
|
||||
};
|
||||
|
||||
// ---- CC : volumes par voix ----
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~kickAmp = val.linlin(0,127, 0,1); ~debouncedReload.() }, 13));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~hatAmp = val.linlin(0,127, 0,0.4); ~debouncedReload.() }, 14));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~snareAmp = val.linlin(0,127, 0,0.8); ~debouncedReload.() }, 15));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~clapAmp = val.linlin(0,127, 0,0.6); ~debouncedReload.() }, 16));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~percAmp = val.linlin(0,127, 0,0.5); ~debouncedReload.() }, 17));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~acidAmp = val.linlin(0,127, 0,0.5); ~debouncedReload.() }, 18));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyAmp = val.linlin(0,127, 0,0.4); ~debouncedReload.() }, 19));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~harmonyAmp = val.linlin(0,127, 0,0.4); ~debouncedReload.() }, 20));
|
||||
|
||||
// ---- CC : filtres / drive / bpm ----
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyCutoff = val.linexp(0,127, 100,12000); ~debouncedReload.() }, 29));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyRq = val.linlin(0,127, 0.05,0.8); ~debouncedReload.() }, 30));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~acidCutoff = val.linexp(0,127, 100,8000); ~debouncedReload.() }, 31));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~acidRq = val.linlin(0,127, 0.05,0.5); ~debouncedReload.() }, 32));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyDrive = val.linlin(0,127, 0.5,5.0); ~debouncedReload.() }, 33));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~acidDrive = val.linlin(0,127, 0.5,5.0); ~debouncedReload.() }, 34));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyBoost = val.linlin(0,127, 0.5,3.0); ~debouncedReload.() }, 35));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| TempoClock.default.tempo = val.linlin(0,127, 60,200) / 60 }, 36));
|
||||
|
||||
// ---- CC : wobble / sidechain / FX ----
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyWobbleRate = val.linexp(0,127, 0.5,32); ~debouncedReload.() }, 49));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodyWobbleDepth = val.linlin(0,127, 0,4500); ~debouncedReload.() }, 50));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~melodySubAmt = val.linlin(0,127, 0,1.5); ~debouncedReload.() }, 51));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~acidScAmt = val.linlin(0,127, 0,1); ~debouncedReload.() }, 52));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~leadScAmt = val.linlin(0,127, 0,1); ~debouncedReload.() }, 53));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val| ~kickScShape = val.linlin(0,127, 0.05,0.5); ~debouncedReload.() }, 54));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val|
|
||||
~fxFx !? { ~fxFx[\rev] !? { |s| s.set(\mix, val.linlin(0,127, 0,1)) } };
|
||||
}, 55));
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val|
|
||||
~fxFx !? { ~fxFx[\dly] !? { |s| s.set(\feedback, val.linlin(0,127, 0,0.95)) } };
|
||||
}, 56));
|
||||
|
||||
// ---- CC : master ----
|
||||
~midiFuncs.add(MIDIFunc.cc({ |val|
|
||||
~masterVol !? { ~masterVol.(val.linlin(0,127, 0, 1.5)) };
|
||||
}, 7));
|
||||
|
||||
// ---- Notes 36-47 : albums A-L ----
|
||||
12.do { |i|
|
||||
var letter = "ABCDEFGHIJKL"[i].asString.asSymbol;
|
||||
~midiFuncs.add(MIDIFunc.noteOn({
|
||||
~playAlbum !? { ~playAlbum.(letter) };
|
||||
}, 36 + i));
|
||||
};
|
||||
// ---- Notes 48-59 : albums M-X ----
|
||||
12.do { |i|
|
||||
var letter = "MNOPQRSTUVWX"[i].asString.asSymbol;
|
||||
~midiFuncs.add(MIDIFunc.noteOn({
|
||||
~playAlbum !? { ~playAlbum.(letter) };
|
||||
}, 48 + i));
|
||||
};
|
||||
// ---- Notes 60-71 : kicks 0..11 ----
|
||||
12.do { |i|
|
||||
~midiFuncs.add(MIDIFunc.noteOn({ ~kkAt.(i) }, 60 + i));
|
||||
};
|
||||
// ---- Notes 72-83 : melodies 0..11 ----
|
||||
12.do { |i|
|
||||
~midiFuncs.add(MIDIFunc.noteOn({ ~mmAt.(i) }, 72 + i));
|
||||
};
|
||||
// ---- Notes 84-95 : fx 0..11 ----
|
||||
12.do { |i|
|
||||
~midiFuncs.add(MIDIFunc.noteOn({ ~ffAt.(i) }, 84 + i));
|
||||
};
|
||||
|
||||
"[midi_init] mapping default applique :".postln;
|
||||
(" notes 36-59 = albums A-X (~playAlbum)").postln;
|
||||
(" notes 60-71 = kicks 0-11 (~kkAt)").postln;
|
||||
(" notes 72-83 = melodies 0-11 (~mmAt)").postln;
|
||||
(" notes 84-95 = fx 0-11 (~ffAt)").postln;
|
||||
(" CC 7 = master volume").postln;
|
||||
(" CC 13-20 = volumes par voix").postln;
|
||||
(" CC 29-36 = cutoffs / drive / bpm").postln;
|
||||
(" CC 49-56 = wobble / sidechain / FX").postln;
|
||||
};
|
||||
|
||||
// Defer init so MIDIClient discovery doesn't block the main load
|
||||
AppClock.sched(0.5, {
|
||||
~midiSafe.({
|
||||
MIDIClient.init;
|
||||
var nSrc = MIDIClient.sources.size;
|
||||
("[midi_init] " ++ nSrc ++ " MIDI source(s) detected").postln;
|
||||
if (nSrc > 0) {
|
||||
MIDIClient.sources.do { |src, i|
|
||||
(" [" ++ i ++ "] " ++ src.device ++ " : " ++ src.name).postln;
|
||||
};
|
||||
MIDIIn.connectAll;
|
||||
~midiMapDefault.value;
|
||||
~midiInitialized = true;
|
||||
"[midi_init] OK".postln;
|
||||
} {
|
||||
"[midi_init] no MIDI device, skipping mapping (re-run live/midi_init.scd to retry)".postln;
|
||||
~midiInitialized = false;
|
||||
};
|
||||
}, "midi_init");
|
||||
nil;
|
||||
});
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
// Hydra patch — coller dans https://hydra.ojack.xyz/
|
||||
// Lance hydra-sc-bridge dans un terminal AVANT, et SC envoie OSC sur 8000
|
||||
// Variables exposées par le bridge : a.fft[0..2] (low/mid/high) si SuperDirt-RMS-style
|
||||
|
||||
a.show()
|
||||
a.setBins(3)
|
||||
a.setSmooth(0.85)
|
||||
|
||||
osc(() => 10 + (a.fft[0] || 0) * 40, 0.1, () => (a.fft[1] || 0))
|
||||
.color(() => 1 - (a.fft[2] || 0), 0.5, 1)
|
||||
.rotate(() => time * 0.1)
|
||||
.modulate(noise(2, 0.3))
|
||||
.kaleid(5)
|
||||
.out()
|
||||
@@ -0,0 +1,45 @@
|
||||
# Hantek 6022BL helper tools
|
||||
|
||||
Standalone Python scripts that handle the parts of the Hantek 6022BL USB
|
||||
oscilloscope workflow that don't belong in the C++ visualizer
|
||||
(`oscope-of/src/HantekDevice`).
|
||||
|
||||
| Script | Role |
|
||||
|---|---|
|
||||
| `fx2_fwload.py` | One-shot firmware uploader. The Hantek arrives as a bare Cypress FX2LP and needs `fx2lafw-hantek-6022bl.fw` pushed via vendor request 0xA0. After upload the device renumerates with VID:PID 1d50:608e. Uses libusb via ctypes (no `pyusb` dep). |
|
||||
| `hantek_to_osc.py` | Alternative scope reader using `sigrok-cli` (instead of the C++ libusb path in oscope-of). Streams CH1/CH2 chunks as float32 over OSC to `127.0.0.1:57120 /scope`. |
|
||||
| `hantek_osc_bridge.py` | Variant of the above. Same end goal, different parsing strategy. |
|
||||
|
||||
## Prerequisites (macOS)
|
||||
|
||||
1. **PulseView.app** (ships libusb + the firmware blob)
|
||||
```sh
|
||||
brew install --cask pulseview
|
||||
```
|
||||
2. **sigrok-cli** (only needed for the Python scope readers)
|
||||
```sh
|
||||
brew install sigrok-cli
|
||||
```
|
||||
3. **python-osc** (only needed for the Python scope readers)
|
||||
```sh
|
||||
/usr/bin/python3 -m pip install --user python-osc
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Once per session : load the firmware
|
||||
```sh
|
||||
./fx2_fwload.py
|
||||
```
|
||||
This must run before either oscope-of's C++ driver or sigrok-cli can talk to
|
||||
the scope.
|
||||
|
||||
### Then either :
|
||||
- **C++ path (default)** — start `oscope-of` (or the launcher) and the C++
|
||||
`HantekDevice` will open the scope directly via libusb.
|
||||
- **Python path (fallback / debugging)** — start `./hantek_to_osc.py` and
|
||||
open `sound_algo/control/hantek_receiver.scd` in sclang to receive
|
||||
`~hPk1 / ~hRms1 / ~hPk2 / ~hRms2` updated at 50 Hz.
|
||||
|
||||
The two paths are mutually exclusive — only one process can hold the USB
|
||||
device at a time.
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cypress FX2LP firmware loader using libusb1 via ctypes (no pyusb required).
|
||||
Loads fx2lafw-hantek-6022bl.fw onto the Hantek 6022BL via vendor request 0xA0.
|
||||
After upload, the device renumerates with VID:PID 1d50:608e.
|
||||
|
||||
Usage: ./fx2_fwload.py [firmware.fw] (default: bundled in PulseView.app)
|
||||
"""
|
||||
import ctypes, ctypes.util, sys, time, os
|
||||
|
||||
LIBUSB_PATH = "/Applications/PulseView.app/Contents/Frameworks/libusb-1.0.0.dylib"
|
||||
DEFAULT_FW = "/Applications/PulseView.app/Contents/share/sigrok-firmware/fx2lafw-hantek-6022bl.fw"
|
||||
VID_BOOT, PID_BOOT = 0x04b4, 0x6022 # Hantek 6022BL after re-initial unconfigured
|
||||
VID_LV, PID_LV = 0x0925, 0x3881 # Lakeview default (unprogrammed FX2 with EEPROM)
|
||||
|
||||
usb = ctypes.CDLL(LIBUSB_PATH)
|
||||
usb.libusb_init.argtypes = [ctypes.c_void_p]
|
||||
usb.libusb_open_device_with_vid_pid.restype = ctypes.c_void_p
|
||||
usb.libusb_control_transfer.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_uint8,
|
||||
ctypes.c_uint16, ctypes.c_uint16,
|
||||
ctypes.c_char_p, ctypes.c_uint16, ctypes.c_uint
|
||||
]
|
||||
usb.libusb_control_transfer.restype = ctypes.c_int
|
||||
|
||||
CPUCS = 0xE600
|
||||
A0 = 0xA0
|
||||
HOST_TO_DEV = 0x40 # vendor, host→dev
|
||||
|
||||
def vendor_write(handle, addr, data):
|
||||
return usb.libusb_control_transfer(
|
||||
handle, HOST_TO_DEV, A0, addr, 0,
|
||||
ctypes.c_char_p(data), len(data), 1000
|
||||
)
|
||||
|
||||
def main():
|
||||
fw_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_FW
|
||||
if not os.path.isfile(fw_path):
|
||||
sys.exit(f"firmware not found: {fw_path}")
|
||||
|
||||
fw = open(fw_path, "rb").read()
|
||||
print(f"[fx2load] firmware {fw_path} ({len(fw)} B)")
|
||||
|
||||
if usb.libusb_init(None) != 0:
|
||||
sys.exit("libusb_init failed")
|
||||
|
||||
handle = None
|
||||
for vid, pid in [(VID_LV, PID_LV), (VID_BOOT, PID_BOOT)]:
|
||||
h = usb.libusb_open_device_with_vid_pid(None, vid, pid)
|
||||
if h:
|
||||
handle = h
|
||||
print(f"[fx2load] opened {vid:04x}:{pid:04x}")
|
||||
break
|
||||
if not handle:
|
||||
sys.exit("device not found (VID:PID 0925:3881 ou 04b4:6022)")
|
||||
|
||||
usb.libusb_detach_kernel_driver(handle, 0)
|
||||
if usb.libusb_claim_interface(handle, 0) != 0:
|
||||
print("[fx2load] warning: could not claim interface 0 (continuing)")
|
||||
|
||||
# 1. Reset 8051
|
||||
print("[fx2load] reset CPU…")
|
||||
vendor_write(handle, CPUCS, b"\x01")
|
||||
time.sleep(0.05)
|
||||
|
||||
# 2. Upload firmware in 4096-byte chunks
|
||||
chunk = 4096
|
||||
for i in range(0, len(fw), chunk):
|
||||
block = fw[i:i+chunk]
|
||||
n = vendor_write(handle, i, block)
|
||||
if n != len(block):
|
||||
sys.exit(f"upload failed at offset {i}: got {n}")
|
||||
print(f"[fx2load] uploaded {len(fw)} bytes")
|
||||
|
||||
# 3. Release reset
|
||||
print("[fx2load] release CPU…")
|
||||
vendor_write(handle, CPUCS, b"\x00")
|
||||
time.sleep(0.5)
|
||||
|
||||
usb.libusb_release_interface(handle, 0)
|
||||
usb.libusb_close(handle)
|
||||
usb.libusb_exit(None)
|
||||
print("[fx2load] OK — device should renumerate as 1d50:608e (sigrok) ou 04b5:6022 (hantek)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hantek 6022BE/BL → OSC bridge (no sigrok-cli, no pyusb).
|
||||
Uses libusb bundled in PulseView.app via ctypes.
|
||||
|
||||
Protocole post-firmware (fx2lafw hantek-6022be):
|
||||
VR 0xE0 CH1 voltage range (0x01=5V, 0x02=2.5V, 0x05=1V, 0x0A=500mV)
|
||||
VR 0xE1 CH2 voltage range (idem)
|
||||
VR 0xE2 sample rate index (0x01=100kS/s, 0x08=1MS/s, 0x10=4MS/s, 0x18=24MS/s)
|
||||
VR 0xE3 trigger / start (envoyer 0x01)
|
||||
VR 0xE4 num channels (0x01=CH1 only, 0x02=CH1+CH2)
|
||||
Bulk IN endpoint 0x86 payload: échantillons 8-bit non-signés, interleaved
|
||||
|
||||
Usage:
|
||||
./hantek_osc_bridge.py [--rate 1M] [--host 127.0.0.1] [--port 57120]
|
||||
"""
|
||||
import ctypes, ctypes.util, sys, time, argparse, struct
|
||||
from pythonosc.udp_client import SimpleUDPClient
|
||||
|
||||
LIBUSB_PATH = "/Applications/PulseView.app/Contents/Frameworks/libusb-1.0.0.dylib"
|
||||
|
||||
# Hantek post-firmware IDs
|
||||
CANDIDATES = [
|
||||
(0x04b5, 0x602a), # 6022BL programmé
|
||||
(0x04b5, 0x6022), # 6022BE programmé
|
||||
(0x1d50, 0x608e), # sigrok generic fx2lafw
|
||||
]
|
||||
|
||||
RATES = {"100k":0x01, "200k":0x08, "500k":0x10, "1M":0x18,
|
||||
"4M":0x20, "8M":0x28, "16M":0x30, "24M":0x31, "30M":0x32, "48M":0x33}
|
||||
|
||||
# libusb ctypes setup
|
||||
usb = ctypes.CDLL(LIBUSB_PATH)
|
||||
usb.libusb_init.argtypes = [ctypes.c_void_p]
|
||||
usb.libusb_open_device_with_vid_pid.restype = ctypes.c_void_p
|
||||
usb.libusb_control_transfer.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_uint8,
|
||||
ctypes.c_uint16, ctypes.c_uint16,
|
||||
ctypes.c_char_p, ctypes.c_uint16, ctypes.c_uint
|
||||
]
|
||||
usb.libusb_control_transfer.restype = ctypes.c_int
|
||||
usb.libusb_bulk_transfer.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_uint8,
|
||||
ctypes.c_char_p, ctypes.c_int,
|
||||
ctypes.POINTER(ctypes.c_int), ctypes.c_uint
|
||||
]
|
||||
usb.libusb_bulk_transfer.restype = ctypes.c_int
|
||||
|
||||
HOST_TO_DEV = 0x40
|
||||
|
||||
def vendor(handle, req, val_byte):
|
||||
return usb.libusb_control_transfer(
|
||||
handle, HOST_TO_DEV, req, 0, 0,
|
||||
ctypes.c_char_p(bytes([val_byte])), 1, 1000
|
||||
)
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--rate", default="1M", choices=list(RATES))
|
||||
p.add_argument("--host", default="127.0.0.1")
|
||||
p.add_argument("--port", type=int, default=57120)
|
||||
p.add_argument("--addr", default="/scope")
|
||||
p.add_argument("--chunk", type=int, default=4096) # samples per OSC packet
|
||||
args = p.parse_args()
|
||||
|
||||
if usb.libusb_init(None) != 0:
|
||||
sys.exit("libusb_init failed")
|
||||
|
||||
handle = None
|
||||
for vid, pid in CANDIDATES:
|
||||
h = usb.libusb_open_device_with_vid_pid(None, vid, pid)
|
||||
if h:
|
||||
handle = h
|
||||
print(f"[bridge] device {vid:04x}:{pid:04x}")
|
||||
break
|
||||
if not handle:
|
||||
sys.exit("Hantek non trouvé. Lance OpenHantek une fois pour uploader le firmware.")
|
||||
|
||||
r = usb.libusb_set_configuration(handle, 1)
|
||||
print(f"[bridge] set_config={r}", flush=True)
|
||||
r = usb.libusb_claim_interface(handle, 0)
|
||||
print(f"[bridge] claim_interface={r}", flush=True)
|
||||
if r < 0:
|
||||
sys.exit("claim_interface failed (try kill OpenHantek / replug)")
|
||||
|
||||
# Configure: 5V range CH1+CH2, 2 channels, sample rate, then start
|
||||
print(f"[bridge] E0={vendor(handle, 0xE0, 0x01)}", flush=True)
|
||||
print(f"[bridge] E1={vendor(handle, 0xE1, 0x01)}", flush=True)
|
||||
print(f"[bridge] E4={vendor(handle, 0xE4, 0x02)}", flush=True)
|
||||
print(f"[bridge] E2={vendor(handle, 0xE2, RATES[args.rate])}", flush=True)
|
||||
print(f"[bridge] E3={vendor(handle, 0xE3, 0x01)}", flush=True)
|
||||
print(f"[bridge] streaming @ {args.rate}S/s → osc://{args.host}:{args.port}{args.addr}", flush=True)
|
||||
|
||||
osc = SimpleUDPClient(args.host, args.port)
|
||||
buf = ctypes.create_string_buffer(args.chunk * 2) # 2 ch * 1 byte
|
||||
transferred = ctypes.c_int(0)
|
||||
|
||||
try:
|
||||
while True:
|
||||
r = usb.libusb_bulk_transfer(handle, 0x86, buf, len(buf),
|
||||
ctypes.byref(transferred), 200)
|
||||
if r != 0 or transferred.value == 0:
|
||||
continue
|
||||
data = buf.raw[:transferred.value]
|
||||
# interleaved CH1, CH2, CH1, CH2, ...
|
||||
n = len(data) // 2
|
||||
ch1 = [(data[i*2] - 128) / 128.0 for i in range(n)]
|
||||
ch2 = [(data[i*2+1] - 128) / 128.0 for i in range(n)]
|
||||
pk1 = max(abs(min(ch1)), abs(max(ch1)))
|
||||
pk2 = max(abs(min(ch2)), abs(max(ch2)))
|
||||
rms1 = (sum(x*x for x in ch1)/n)**0.5
|
||||
rms2 = (sum(x*x for x in ch2)/n)**0.5
|
||||
osc.send_message(args.addr, [pk1, rms1, pk2, rms2])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
vendor(handle, 0xE3, 0x00) # stop
|
||||
usb.libusb_release_interface(handle, 0)
|
||||
usb.libusb_close(handle)
|
||||
usb.libusb_exit(None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hantek 6022BL → OSC bridge.
|
||||
Lit des blocs d'échantillons via sigrok-cli et envoie chunks OSC à SC.
|
||||
|
||||
Usage:
|
||||
./hantek_to_osc.py [--rate 100k] [--samples 1024] [--host 127.0.0.1] [--port 57120]
|
||||
|
||||
Prérequis:
|
||||
- sigrok-cli installé (brew install sigrok-cli)
|
||||
- firmware fx2lafw chargé (lance OpenHantek une fois, OU sigrok-cli scanne 2x)
|
||||
- python-osc: /usr/bin/python3 -m pip install --user python-osc
|
||||
"""
|
||||
import argparse, subprocess, sys, struct
|
||||
from pythonosc.udp_client import SimpleUDPClient
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--rate", default="100k")
|
||||
p.add_argument("--samples", type=int, default=1024)
|
||||
p.add_argument("--driver", default="hantek-6xxx")
|
||||
p.add_argument("--host", default="127.0.0.1")
|
||||
p.add_argument("--port", type=int, default=57120)
|
||||
p.add_argument("--addr", default="/scope")
|
||||
args = p.parse_args()
|
||||
|
||||
osc = SimpleUDPClient(args.host, args.port)
|
||||
|
||||
cmd = [
|
||||
"sigrok-cli", "-d", args.driver,
|
||||
"--config", f"samplerate={args.rate}",
|
||||
"-O", "binary",
|
||||
"--continuous",
|
||||
"-C", "CH1,CH2",
|
||||
]
|
||||
print(f"[bridge] {' '.join(cmd)} → {args.host}:{args.port}{args.addr}")
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=0)
|
||||
|
||||
chunk_bytes = args.samples * 4 * 2 # 2 channels float32
|
||||
try:
|
||||
while True:
|
||||
buf = proc.stdout.read(chunk_bytes)
|
||||
if not buf:
|
||||
break
|
||||
n = len(buf) // 8
|
||||
ch1 = struct.unpack(f"{n}f", b"".join(buf[i*8:i*8+4] for i in range(n)))
|
||||
ch2 = struct.unpack(f"{n}f", b"".join(buf[i*8+4:i*8+8] for i in range(n)))
|
||||
# Send peak + RMS per channel (compact, SC-friendly)
|
||||
def pk(s): return max(abs(min(s)), abs(max(s)))
|
||||
def rms(s): return (sum(x*x for x in s)/len(s))**0.5
|
||||
osc.send_message(args.addr, [pk(ch1), rms(ch1), pk(ch2), rms(ch2)])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
proc.terminate()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user