memlnaut-nisps/firmware/useq-celium/main/src/main.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

174 lines
6.4 KiB
C++

// uSEQ-CV main board firmware (protocol v2).
//
// Target: uSEQ USEQHARDWARE_1_0 — Raspberry Pi Pico (RP2040), Arduino-Pico core
// (Earle Philhower). No dependencies beyond Arduino + Wire.
//
// Receives OUTPUT frames over USB serial (host = browser CV backend OR the
// MEMLNaut firmware), drives CV1..CV3 (PWM) + GATE1..GATE3 (digital) locally,
// and forwards CV4..CV11 to the expander over I2C. See ../../shared/protocol.h.
#include <Arduino.h>
#include <Wire.h>
#include "protocol.h"
// ─── Pin map (USEQHARDWARE_1_0) ──────────────────────────────────────────────
constexpr uint8_t PIN_CV[USEQ_NUM_MAIN_CV] = { 21, 20, 19 }; // a1,a2,a3 (PWM)
constexpr uint8_t PIN_GATE[USEQ_NUM_GATE] = { 18, 17, 16 }; // d1,d2,d3 (digital)
constexpr uint8_t PIN_CV_LED[USEQ_NUM_MAIN_CV] = { 3, 2, 11 };
constexpr uint8_t PIN_GATE_LED[USEQ_NUM_GATE] = { 12, 13, 22 };
constexpr uint8_t PIN_I1 = 8, PIN_I2 = 9; // digital inputs
constexpr uint8_t PIN_AI1 = 26, PIN_AI2 = 27; // analog inputs
constexpr uint8_t PIN_SDA = 0, PIN_SCL = 1; // I2C to expander
// ─── State ───────────────────────────────────────────────────────────────────
uint16_t cvValues[USEQ_NUM_CV] = {}; // 12-bit wire values, CV1..CV11
uint8_t gateBits = 0; // bit0..2 = GATE1..3
uint8_t rxBuf[USEQ_FRAME_OUTPUT_LEN];
uint8_t rxIdx = 0;
bool synced = false;
uint32_t lastInputMs = 0, lastI2CMs = 0, lastScanMs = 0;
bool expanderFound = false;
// ─── Setup ───────────────────────────────────────────────────────────────────
void probeExpander() {
Wire.beginTransmission(USEQ_I2C_ADDR);
expanderFound = (Wire.endTransmission() == 0);
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
for (uint8_t i = 0; i < USEQ_NUM_MAIN_CV; i++) {
pinMode(PIN_CV[i], OUTPUT);
pinMode(PIN_CV_LED[i], OUTPUT);
}
for (uint8_t i = 0; i < USEQ_NUM_GATE; i++) {
pinMode(PIN_GATE[i], OUTPUT);
pinMode(PIN_GATE_LED[i], OUTPUT);
}
pinMode(PIN_I1, INPUT);
pinMode(PIN_I2, INPUT);
analogWriteFreq(100000);
analogWriteRange(USEQ_PWM_MAX);
Wire.setSDA(PIN_SDA);
Wire.setSCL(PIN_SCL);
Wire.begin();
Wire.setClock(400000);
Wire.setTimeout(5);
probeExpander();
}
// ─── Frame handlers ──────────────────────────────────────────────────────────
void processOutputFrame() {
if (useq_xor(rxBuf, 1, USEQ_OFF_OXSUM - 1) != rxBuf[USEQ_OFF_OXSUM]) return;
for (uint8_t i = 0; i < USEQ_NUM_CV; i++) {
uint16_t v = useq_read_u16le(&rxBuf[USEQ_OFF_CV0 + i * 2]);
cvValues[i] = (v > USEQ_CV_MAX) ? USEQ_CV_MAX : v;
}
gateBits = rxBuf[USEQ_OFF_GATES];
}
void identifyLedSweep() {
const uint8_t leds[6] = { PIN_CV_LED[0], PIN_CV_LED[1], PIN_CV_LED[2],
PIN_GATE_LED[0], PIN_GATE_LED[1], PIN_GATE_LED[2] };
for (int i = 0; i < 6; i++) { analogWrite(leds[i], USEQ_PWM_MAX); delay(40); }
delay(80);
for (int i = 5; i >= 0; i--) { analogWrite(leds[i], 0); delay(40); }
}
void processIdentifyFrame() {
if (rxBuf[1] != rxBuf[2]) return; // xor of byte 1 only
if (expanderFound) {
Wire.beginTransmission(USEQ_I2C_ADDR);
Wire.write(USEQ_SYNC_I2C_IDENTIFY);
Wire.endTransmission();
}
identifyLedSweep();
uint8_t ack[USEQ_FRAME_ACK_LEN] = { USEQ_SYNC_DEV, USEQ_MSG_IDENTIFY, 0x01, 0 };
ack[3] = ack[1] ^ ack[2];
Serial.write(ack, USEQ_FRAME_ACK_LEN);
}
void readSerial() {
while (Serial.available()) {
uint8_t b = Serial.read();
if (!synced) {
if (b == USEQ_SYNC_HOST) { rxBuf[0] = b; rxIdx = 1; synced = true; }
continue;
}
rxBuf[rxIdx++] = b;
if (rxIdx == 2 && rxBuf[1] != USEQ_MSG_OUTPUT && rxBuf[1] != USEQ_MSG_IDENTIFY) {
synced = false; // unknown type → resync
continue;
}
uint8_t want = (rxBuf[1] == USEQ_MSG_OUTPUT) ? USEQ_FRAME_OUTPUT_LEN
: USEQ_FRAME_IDENTIFY_LEN;
if (rxIdx >= want) {
if (rxBuf[1] == USEQ_MSG_OUTPUT) processOutputFrame();
else processIdentifyFrame();
synced = false;
}
}
}
// ─── Outputs ─────────────────────────────────────────────────────────────────
void writeOutputs() {
for (uint8_t i = 0; i < USEQ_NUM_MAIN_CV; i++)
analogWrite(PIN_CV[i], useq_cv_to_pwm(cvValues[i]));
for (uint8_t i = 0; i < USEQ_NUM_GATE; i++) {
bool on = (gateBits >> i) & 1;
digitalWrite(PIN_GATE[i], on ? HIGH : LOW);
digitalWrite(PIN_GATE_LED[i], on ? HIGH : LOW);
}
for (uint8_t i = 0; i < USEQ_NUM_MAIN_CV; i++) {
uint16_t pwm = useq_cv_to_pwm(cvValues[i]);
analogWrite(PIN_CV_LED[i], (uint16_t)(((uint32_t)pwm * pwm) >> 11));
}
}
void forwardToExpander() {
if (!expanderFound) return;
uint32_t now = millis();
if (now - lastI2CMs < 10) return; // throttle ~100 Hz
lastI2CMs = now;
uint8_t f[USEQ_FRAME_I2C_LEN];
f[0] = USEQ_SYNC_I2C;
for (uint8_t i = 0; i < USEQ_NUM_EXP_CV; i++)
useq_write_u16le(&f[1 + i * 2], useq_cv_to_pwm(cvValues[USEQ_NUM_MAIN_CV + i]));
f[USEQ_FRAME_I2C_LEN - 1] = useq_xor(f, 0, USEQ_FRAME_I2C_LEN - 2);
Wire.beginTransmission(USEQ_I2C_ADDR);
Wire.write(f, USEQ_FRAME_I2C_LEN);
Wire.endTransmission();
}
void retryExpanderScan() {
if (expanderFound) return;
uint32_t now = millis();
if (now - lastScanMs < 2000) return;
lastScanMs = now;
probeExpander();
}
void sendInputs() {
uint32_t now = millis();
if (now - lastInputMs < 50) return; // 20 Hz
lastInputMs = now;
uint8_t f[USEQ_FRAME_INPUT_LEN];
f[0] = USEQ_SYNC_DEV;
f[1] = USEQ_MSG_INPUT;
useq_write_u16le(&f[2], digitalRead(PIN_I1));
useq_write_u16le(&f[4], digitalRead(PIN_I2));
useq_write_u16le(&f[6], analogRead(PIN_AI1));
useq_write_u16le(&f[8], analogRead(PIN_AI2));
f[10] = useq_xor(f, 1, 9);
Serial.write(f, USEQ_FRAME_INPUT_LEN);
}
void loop() {
readSerial();
writeOutputs();
retryExpanderScan();
forwardToExpander();
sendInputs();
}