memlnaut-nisps/firmware/useq-celium/expander/src/expander.cpp
monkey-w1n5t0n 4656568d4f feat(manifold,firmware): restore uSEQ CV/gate as an Outputs backend
Restores the April-2026 "uSEQ-Celium" functionality (browser → uSEQ
hardware + CV expander over USB Web Serial) as a first-class Manifold
Outputs backend, and re-vendors the RP2040 firmware into the repo.

- protocol v2 (uSEQ-CV): firmware/useq-celium/shared/protocol.h is the
  single source of truth, mirrored by manifold/src/backends/useq-protocol.ts.
  26-byte OUTPUT frame, 11×u16 CV (12-bit) + 3-gate bitfield + XOR; fixed
  topology; host-agnostic so the MEMLNaut RP2350 can emit identical bytes.
  Spec in docs/useq-celium/protocol.md.
- firmware/useq-celium/{main,expander}: PlatformIO RP2040 firmware rewritten
  to v2 from the real April pin maps (expander I2C addr 0x10).
- UseqCvBackend (id cvgate): Web Serial connect/identify/disconnect, 100 Hz
  stream, per-channel dead-zone, gate thresholding; modeled on midi-backend.
  Per-output CvSpec (channel + gateThreshold) on MFParam; config UI in
  OutputsBackendConfig + BackendAdvanced; new "CV / uSEQ" top-dock mode.
- bun-test for the protocol frame layout; MAP.md updated.
2026-06-28 22:30:54 +02:00

68 lines
2.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// uSEQ-CV expander firmware (protocol v2).
//
// Target: uSEQ USEQHARDWARE_EXPANDER_OUT_0_1 — Raspberry Pi Pico (RP2040),
// Arduino-Pico core. I2C slave at USEQ_I2C_ADDR; receives 8 × 11-bit CV values
// from the main board and writes them to PWM. See ../../shared/protocol.h.
#include <Arduino.h>
#include <Wire.h>
#include "protocol.h"
// ─── Pin map (USEQHARDWARE_EXPANDER_OUT_0_1) ────────────────────────────────
constexpr uint8_t PIN_E[USEQ_NUM_EXP_CV] = { 13, 14, 10, 11, 8, 7, 5, 3 };
constexpr uint8_t PIN_E_LED[USEQ_NUM_EXP_CV] = { 15, 20, 17, 12, 9, 6, 2, 0 };
constexpr uint8_t PIN_SDA = 4, PIN_SCL = 1;
volatile uint16_t cvValues[USEQ_NUM_EXP_CV] = {};
volatile bool newFrame = false;
volatile bool doSweep = false;
void onI2CReceive(int numBytes) {
if (numBytes == 1) {
if (Wire.read() == USEQ_SYNC_I2C_IDENTIFY) doSweep = true;
return;
}
if (numBytes != USEQ_FRAME_I2C_LEN) {
while (Wire.available()) Wire.read();
return;
}
uint8_t buf[USEQ_FRAME_I2C_LEN];
for (uint8_t i = 0; i < USEQ_FRAME_I2C_LEN; i++) buf[i] = Wire.read();
if (buf[0] != USEQ_SYNC_I2C) return;
if (useq_xor(buf, 0, USEQ_FRAME_I2C_LEN - 2) != buf[USEQ_FRAME_I2C_LEN - 1]) return;
for (uint8_t i = 0; i < USEQ_NUM_EXP_CV; i++) {
uint16_t v = useq_read_u16le(&buf[1 + i * 2]);
cvValues[i] = (v > USEQ_PWM_MAX) ? USEQ_PWM_MAX : v;
}
newFrame = true;
}
void ledSweep() {
for (int i = 0; i < USEQ_NUM_EXP_CV; i++) { analogWrite(PIN_E_LED[i], USEQ_PWM_MAX); delay(40); }
delay(80);
for (int i = USEQ_NUM_EXP_CV - 1; i >= 0; i--) { analogWrite(PIN_E_LED[i], 0); delay(40); }
}
void setup() {
for (uint8_t i = 0; i < USEQ_NUM_EXP_CV; i++) {
pinMode(PIN_E[i], OUTPUT);
pinMode(PIN_E_LED[i], OUTPUT);
}
analogWriteFreq(100000);
analogWriteRange(USEQ_PWM_MAX);
Wire.setSDA(PIN_SDA);
Wire.setSCL(PIN_SCL);
Wire.begin(USEQ_I2C_ADDR);
Wire.onReceive(onI2CReceive);
}
void loop() {
if (doSweep) { doSweep = false; ledSweep(); }
if (newFrame) {
newFrame = false;
for (uint8_t i = 0; i < USEQ_NUM_EXP_CV; i++) {
uint16_t v = cvValues[i];
analogWrite(PIN_E[i], v);
analogWrite(PIN_E_LED[i], (uint16_t)(((uint32_t)v * v) >> 11));
}
}
}