New CPU/Clock OLED screen (kScreenCpu), between Diagnostics and BT Signal in the K0 cycle. Shows: - Set - configured system clock (SYS_CLOCK_KHZ overclock target) - Real - clk_sys from the RP2350 frequency counter, measured once on screen entry and cached (it is fixed at boot; avoids a per-frame multi-ms stall) - Vcore - core voltage read back via vreg_get_voltage() - Temp - on-die sensor via cpu_temp_raw_smoothed() render_screen_cpu() is noinline like the other render functions (Thumb literal-pool reach). Hardware-in-the-loop verified on Pico 2 W + Waveshare Pico-OLED-1.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1286 lines
46 KiB
C++
1286 lines
46 KiB
C++
#include "oled.h"
|
|
#include "oled_font.h"
|
|
#include "bt.h"
|
|
#include "slots.h"
|
|
#include "audio.h"
|
|
#include "config.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include "hardware/spi.h"
|
|
#include "hardware/gpio.h"
|
|
#include "hardware/watchdog.h"
|
|
#include "hardware/clocks.h"
|
|
#include "hardware/vreg.h"
|
|
#include "cmd.h"
|
|
#include "pico/time.h"
|
|
|
|
extern uint8_t interrupt_in_data[63]; // defined in main.cpp
|
|
|
|
namespace {
|
|
|
|
constexpr uint kPinDC = 8;
|
|
constexpr uint kPinCS = 9;
|
|
constexpr uint kPinCLK = 10;
|
|
constexpr uint kPinMOSI = 11;
|
|
constexpr uint kPinRST = 12;
|
|
constexpr uint kPinKey0 = 15;
|
|
constexpr uint kPinKey1 = 17;
|
|
|
|
constexpr int kW = 128;
|
|
constexpr int kH = 64;
|
|
constexpr int kRowBytes = kW / 8;
|
|
constexpr int kFbBytes = kRowBytes * kH;
|
|
|
|
uint8_t fb[kFbBytes];
|
|
|
|
uint32_t last_render_us = 0;
|
|
constexpr uint32_t kFrameUs = 100000;
|
|
bool key0_prev = true;
|
|
bool key1_prev = true;
|
|
uint32_t key0_t_us = 0;
|
|
uint32_t key1_t_us = 0;
|
|
constexpr uint32_t kDebounceUs = 20000;
|
|
|
|
uint32_t key0_first_press_us = 0;
|
|
bool key0_pending_single = false;
|
|
constexpr uint32_t kDoubleClickUs = 400000;
|
|
|
|
// KEY1 long-press detection (for brightness cycling)
|
|
uint32_t key1_press_us = 0;
|
|
bool key1_was_pressed = false;
|
|
constexpr uint32_t kLongPressUs = 1500000;
|
|
|
|
// Brightness levels (SH1107 contrast register 0x81). User cycles via KEY1 long-press.
|
|
constexpr uint8_t kBrightLevels[] = {0xFF, 0x7F, 0x3F, 0x10};
|
|
constexpr int kNumBrightLevels = sizeof(kBrightLevels) / sizeof(kBrightLevels[0]);
|
|
int bright_idx = 0;
|
|
uint8_t current_contrast = 0xFF;
|
|
|
|
// Auto-dim after idle. Tracks last button/input activity.
|
|
uint32_t last_activity_us = 0;
|
|
uint32_t last_input_hash = 0;
|
|
constexpr uint32_t kAutoDimUs = 5UL * 60UL * 1000000UL; // 5 min
|
|
constexpr uint8_t kDimContrast = 0x10;
|
|
|
|
// Screen ordering — single source of truth. Reorder by editing this block;
|
|
// oled_loop's switch and handle_buttons' KEY1 contextual checks use these
|
|
// names, so the indices can move without touching that code.
|
|
constexpr int kScreenStatus = 0;
|
|
constexpr int kScreenSlots = 1;
|
|
constexpr int kScreenLightbar = 2;
|
|
constexpr int kScreenTriggers = 3;
|
|
constexpr int kScreenGyro = 4;
|
|
constexpr int kScreenTouchpad = 5;
|
|
constexpr int kScreenDiag = 6;
|
|
constexpr int kScreenCpu = 7;
|
|
constexpr int kScreenRssi = 8;
|
|
constexpr int kScreenVU = 9;
|
|
constexpr int kScreenSettings = 10;
|
|
constexpr int kNumScreens = 11;
|
|
int current_screen = 0;
|
|
|
|
// Lightbar mode cycle: 0=LIVE, 1-4=FAV0-3, 5=BREATHING, 6=RAINBOW, 7=FADE
|
|
constexpr int kNumLbModes = 8;
|
|
|
|
// Settings screen state
|
|
constexpr int kNumSettingsItems = 13; // 8 fields + 3 auto-haptic + Reset + Wipe
|
|
constexpr int kSettingsAutoHapEnaIdx = 8;
|
|
constexpr int kSettingsAutoHapGainIdx = 9;
|
|
constexpr int kSettingsAutoHapLpIdx = 10;
|
|
constexpr int kSettingsResetIdx = 11;
|
|
constexpr int kSettingsWipeSlotsIdx = 12;
|
|
Config_body settings_local{};
|
|
int settings_sel = 0;
|
|
bool settings_dirty = false;
|
|
bool settings_init_done = false;
|
|
uint8_t settings_last_dpad = 8; // 8 = released
|
|
uint8_t settings_last_face = 0;
|
|
const char* settings_save_status = ""; // shown briefly after Triangle press
|
|
|
|
// Factory-reset hold-Triangle-2s state. Borrowed from zurce/DS5Dongle-OLED's
|
|
// "hold to wipe" UX pattern (https://github.com/zurce/DS5Dongle-OLED).
|
|
uint32_t settings_tri_press_us = 0;
|
|
bool settings_reset_triggered = false;
|
|
constexpr uint32_t kResetHoldUs = 2000000;
|
|
|
|
uint8_t lb_r = 0, lb_g = 0, lb_b = 0;
|
|
|
|
// Lightbar mode + favorite slots: 0 = LIVE tilt preview; 1..4 = saved slots F0..F3
|
|
int lb_mode = 0;
|
|
uint8_t lb_fav_r[4] = {255, 0, 0, 255}; // Red, Green, Blue, White defaults
|
|
uint8_t lb_fav_g[4] = {0, 255, 0, 255};
|
|
uint8_t lb_fav_b[4] = {0, 0, 255, 255};
|
|
uint8_t lb_last_face = 0;
|
|
|
|
uint32_t rumble_off_at_us = 0;
|
|
bool rumble_active = false;
|
|
constexpr uint32_t kRumbleBurstUs = 250000;
|
|
|
|
int trigger_preset = 0;
|
|
const char* const kTrigPresetNames[] = {"Off", "Feedback", "Weapon", "Vibration", "Bow", "Gallop", "Machine"};
|
|
constexpr int kNumTrigPresets = 7;
|
|
|
|
void cmd(uint8_t c) {
|
|
gpio_put(kPinDC, 0);
|
|
gpio_put(kPinCS, 0);
|
|
spi_write_blocking(spi1, &c, 1);
|
|
gpio_put(kPinCS, 1);
|
|
}
|
|
|
|
void data_byte(uint8_t d) {
|
|
gpio_put(kPinDC, 1);
|
|
gpio_put(kPinCS, 0);
|
|
spi_write_blocking(spi1, &d, 1);
|
|
gpio_put(kPinCS, 1);
|
|
}
|
|
|
|
uint8_t reverse_byte(uint8_t b) {
|
|
b = ((b & 0x55) << 1) | ((b & 0xAA) >> 1);
|
|
b = ((b & 0x33) << 2) | ((b & 0xCC) >> 2);
|
|
b = ((b & 0x0F) << 4) | ((b & 0xF0) >> 4);
|
|
return b;
|
|
}
|
|
|
|
void hw_reset() {
|
|
gpio_put(kPinRST, 1); sleep_ms(100);
|
|
gpio_put(kPinRST, 0); sleep_ms(100);
|
|
gpio_put(kPinRST, 1); sleep_ms(100);
|
|
}
|
|
|
|
void sh1107_set_contrast(uint8_t value) {
|
|
if (value == current_contrast) return;
|
|
current_contrast = value;
|
|
cmd(0x81); cmd(value);
|
|
}
|
|
|
|
void sh1107_init() {
|
|
cmd(0xAE);
|
|
cmd(0x00); cmd(0x10);
|
|
cmd(0xB0);
|
|
cmd(0xDC); cmd(0x00);
|
|
cmd(0x81); cmd(0x6F);
|
|
cmd(0x21);
|
|
cmd(0xA0);
|
|
cmd(0xC0);
|
|
cmd(0xA4);
|
|
cmd(0xA6);
|
|
cmd(0xA8); cmd(0x3F);
|
|
cmd(0xD3); cmd(0x60);
|
|
cmd(0xD5); cmd(0x41);
|
|
cmd(0xD9); cmd(0x22);
|
|
cmd(0xDB); cmd(0x35);
|
|
cmd(0xAD); cmd(0x8A);
|
|
sleep_ms(50);
|
|
cmd(0xAF);
|
|
}
|
|
|
|
void flush_fb() {
|
|
cmd(0xB0);
|
|
for (int j = 0; j < kH; j++) {
|
|
const uint8_t col = kH - 1 - j;
|
|
cmd(0x00 + (col & 0x0F));
|
|
cmd(0x10 + (col >> 4));
|
|
for (int i = 0; i < kRowBytes; i++) {
|
|
data_byte(reverse_byte(fb[j * kRowBytes + i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
void fb_clear() { memset(fb, 0, sizeof(fb)); }
|
|
|
|
void px(int x, int y, bool on) {
|
|
if (x < 0 || x >= kW || y < 0 || y >= kH) return;
|
|
uint8_t *p = &fb[y * kRowBytes + (x / 8)];
|
|
uint8_t m = 1 << (7 - (x % 8));
|
|
if (on) *p |= m; else *p &= ~m;
|
|
}
|
|
|
|
void rect_outline(int x, int y, int w, int h) {
|
|
for (int i = 0; i < w; i++) { px(x + i, y, true); px(x + i, y + h - 1, true); }
|
|
for (int i = 0; i < h; i++) { px(x, y + i, true); px(x + w - 1, y + i, true); }
|
|
}
|
|
|
|
void rect_filled(int x, int y, int w, int h) {
|
|
for (int j = 0; j < h; j++)
|
|
for (int i = 0; i < w; i++)
|
|
px(x + i, y + j, true);
|
|
}
|
|
|
|
void draw_char(int x, int y, char c) {
|
|
if (c < 0x20 || c > 0x7E) return;
|
|
const uint8_t *g = kFont5x7[c - 0x20];
|
|
for (int col = 0; col < kFontW; col++) {
|
|
uint8_t bits = g[col];
|
|
for (int row = 0; row < kFontH; row++) {
|
|
if (bits & (1 << row)) px(x + col, y + row, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void draw_text(int x, int y, const char *s) {
|
|
while (*s) {
|
|
draw_char(x, y, *s++);
|
|
x += 6;
|
|
}
|
|
}
|
|
|
|
// Pixel-art icon support. Visual approach inspired by zurce/DS5Dongle-OLED
|
|
// (https://github.com/zurce/DS5Dongle-OLED) — credit to zurce for the idea
|
|
// of decorating the OLED with small bitmaps instead of bare text/shapes.
|
|
// Bitmap layout: row-major, MSB = leftmost pixel, ceil(w/8) bytes per row.
|
|
void draw_icon(int x, int y, const uint8_t *bitmap, int w, int h) {
|
|
const int row_bytes = (w + 7) / 8;
|
|
for (int row = 0; row < h; row++) {
|
|
for (int col = 0; col < w; col++) {
|
|
const uint8_t byte = bitmap[row * row_bytes + (col / 8)];
|
|
const uint8_t mask = (uint8_t)(1u << (7 - (col % 8)));
|
|
if (byte & mask) px(x + col, y + row, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 8x8 "link active" filled circle (drawn when DS5 is paired)
|
|
static const uint8_t kIconLinkOn[8] = {
|
|
0b00111100,
|
|
0b01111110,
|
|
0b11111111,
|
|
0b11111111,
|
|
0b11111111,
|
|
0b11111111,
|
|
0b01111110,
|
|
0b00111100,
|
|
};
|
|
// 8x8 "link inactive" hollow circle (drawn when waiting for DS5)
|
|
static const uint8_t kIconLinkOff[8] = {
|
|
0b00111100,
|
|
0b01000010,
|
|
0b10000001,
|
|
0b10000001,
|
|
0b10000001,
|
|
0b10000001,
|
|
0b01000010,
|
|
0b00111100,
|
|
};
|
|
|
|
// Battery icon — body 52x8 + small nub on the right. Inside fill scales with pct.
|
|
void draw_battery_icon(int x, int y, int pct) {
|
|
rect_outline(x, y, 52, 8);
|
|
rect_filled(x + 52, y + 2, 3, 4);
|
|
int fill = (pct * 48) / 100;
|
|
if (fill < 0) fill = 0;
|
|
if (fill > 48) fill = 48;
|
|
if (fill > 0) rect_filled(x + 2, y + 2, fill, 4);
|
|
}
|
|
|
|
void send_rumble(uint8_t amplitude) {
|
|
uint8_t pkt[78] = {};
|
|
pkt[0] = 0x31;
|
|
pkt[1] = 0x00;
|
|
pkt[2] = 0x10;
|
|
pkt[3] = 0x03;
|
|
pkt[5] = amplitude;
|
|
pkt[6] = amplitude;
|
|
bt_write(pkt, sizeof(pkt));
|
|
}
|
|
|
|
void rumble_burst_tick(uint32_t now) {
|
|
if (rumble_active && (int32_t)(now - rumble_off_at_us) >= 0) {
|
|
send_rumble(0);
|
|
rumble_active = false;
|
|
}
|
|
}
|
|
|
|
// Trigger effect param format follows dualsensectl's reverse-engineering.
|
|
// Modes 0x21/0x25/0x26 use bitpacked 10-zone arrays, not raw position bytes.
|
|
void send_trigger_effect(int preset) {
|
|
uint8_t pkt[78] = {};
|
|
pkt[0] = 0x31;
|
|
pkt[2] = 0x10;
|
|
pkt[3] = 0x0C; // valid_flag0: RIGHT_TRIGGER_MOTOR_ENABLE | LEFT_TRIGGER_MOTOR_ENABLE
|
|
|
|
uint8_t mode = 0x05; // OFF
|
|
uint8_t p[9] = {0};
|
|
|
|
switch (preset) {
|
|
case 0: // Off
|
|
mode = 0x05;
|
|
break;
|
|
case 1: { // Feedback — all 10 zones at max strength 8
|
|
mode = 0x21;
|
|
const uint16_t active = 0x03FF;
|
|
uint32_t strength = 0;
|
|
for (int i = 0; i < 10; i++) strength |= (uint32_t)(7u << (3 * i));
|
|
p[0] = active & 0xFF;
|
|
p[1] = (active >> 8) & 0xFF;
|
|
p[2] = strength & 0xFF;
|
|
p[3] = (strength >> 8) & 0xFF;
|
|
p[4] = (strength >> 16) & 0xFF;
|
|
p[5] = (strength >> 24) & 0xFF;
|
|
break;
|
|
}
|
|
case 2: { // Weapon — snap between positions 3 and 5, force 8
|
|
mode = 0x25;
|
|
const uint16_t start_stop = (1u << 3) | (1u << 5);
|
|
p[0] = start_stop & 0xFF;
|
|
p[1] = (start_stop >> 8) & 0xFF;
|
|
p[2] = 7; // force = strength - 1
|
|
break;
|
|
}
|
|
case 3: { // Vibration — all 10 zones at amplitude 8, frequency 30 Hz
|
|
mode = 0x26;
|
|
const uint16_t active = 0x03FF;
|
|
uint32_t strength = 0;
|
|
for (int i = 0; i < 10; i++) strength |= (uint32_t)(7u << (3 * i));
|
|
p[0] = active & 0xFF;
|
|
p[1] = (active >> 8) & 0xFF;
|
|
p[2] = strength & 0xFF;
|
|
p[3] = (strength >> 8) & 0xFF;
|
|
p[4] = (strength >> 16) & 0xFF;
|
|
p[5] = (strength >> 24) & 0xFF;
|
|
p[8] = 30;
|
|
break;
|
|
}
|
|
case 4: { // Bow — drawing resistance + snap at position 6
|
|
mode = 0x22;
|
|
const uint16_t start_stop = (1u << 2) | (1u << 6);
|
|
const uint8_t force_pair = 7u | (7u << 3); // strength=8, snap=8
|
|
p[0] = start_stop & 0xFF;
|
|
p[1] = (start_stop >> 8) & 0xFF;
|
|
p[2] = force_pair;
|
|
break;
|
|
}
|
|
case 5: { // Galloping
|
|
mode = 0x23;
|
|
const uint16_t start_stop = (1u << 0) | (1u << 9);
|
|
const uint8_t ratio = (5u & 0x07) | ((1u & 0x07) << 3);
|
|
p[0] = start_stop & 0xFF;
|
|
p[1] = (start_stop >> 8) & 0xFF;
|
|
p[2] = ratio;
|
|
p[3] = 5; // frequency
|
|
break;
|
|
}
|
|
case 6: { // Machine gun
|
|
mode = 0x27;
|
|
const uint16_t start_stop = (1u << 1) | (1u << 8);
|
|
const uint8_t force_pair = 7u | (7u << 3);
|
|
p[0] = start_stop & 0xFF;
|
|
p[1] = (start_stop >> 8) & 0xFF;
|
|
p[2] = force_pair;
|
|
p[3] = 20; // frequency
|
|
p[4] = 0; // period
|
|
break;
|
|
}
|
|
}
|
|
|
|
pkt[13] = mode;
|
|
for (int i = 0; i < 9; i++) pkt[14 + i] = p[i];
|
|
pkt[24] = mode;
|
|
for (int i = 0; i < 9; i++) pkt[25 + i] = p[i];
|
|
|
|
bt_write(pkt, sizeof(pkt));
|
|
}
|
|
|
|
void send_lightbar_color(uint8_t r, uint8_t g, uint8_t b);
|
|
|
|
void handle_buttons() {
|
|
const uint32_t now = time_us_32();
|
|
const bool k0 = gpio_get(kPinKey0);
|
|
const bool k1 = gpio_get(kPinKey1);
|
|
if (!k0 && key0_prev && (now - key0_t_us) > kDebounceUs) {
|
|
key0_t_us = now;
|
|
if (key0_pending_single && (now - key0_first_press_us) < kDoubleClickUs) {
|
|
key0_pending_single = false;
|
|
watchdog_reboot(0, 0, 0);
|
|
} else {
|
|
key0_pending_single = true;
|
|
key0_first_press_us = now;
|
|
}
|
|
}
|
|
if (key0_pending_single && (now - key0_first_press_us) > kDoubleClickUs) {
|
|
key0_pending_single = false;
|
|
current_screen = (current_screen + 1) % kNumScreens;
|
|
last_render_us = 0;
|
|
}
|
|
// KEY1: track press time, decide on release whether it was short or long
|
|
if (!k1 && key1_prev && (now - key1_t_us) > kDebounceUs) {
|
|
key1_t_us = now;
|
|
key1_press_us = now;
|
|
key1_was_pressed = true;
|
|
last_activity_us = now;
|
|
}
|
|
if (k1 && !key1_prev && key1_was_pressed) {
|
|
key1_was_pressed = false;
|
|
const uint32_t held = now - key1_press_us;
|
|
last_activity_us = now;
|
|
if (held > kLongPressUs) {
|
|
// Long press: cycle brightness level
|
|
bright_idx = (bright_idx + 1) % kNumBrightLevels;
|
|
} else {
|
|
// Short press: contextual per screen. On Trigger Test / Lightbar
|
|
// KEY1 stays as the primary in-screen cycle. Everywhere else it
|
|
// acts as a "back" button — cycles to the previous screen so
|
|
// KEY0/KEY1 form a natural forward/back pair.
|
|
if (current_screen == kScreenTriggers) {
|
|
trigger_preset = (trigger_preset + 1) % kNumTrigPresets;
|
|
send_trigger_effect(trigger_preset);
|
|
} else if (current_screen == kScreenLightbar) {
|
|
lb_mode = (lb_mode + 1) % kNumLbModes;
|
|
} else {
|
|
current_screen = (current_screen - 1 + kNumScreens) % kNumScreens;
|
|
last_render_us = 0;
|
|
}
|
|
}
|
|
}
|
|
key0_prev = k0;
|
|
key1_prev = k1;
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen() {
|
|
fb_clear();
|
|
|
|
const bool connected = bt_is_connected();
|
|
|
|
draw_text(0, 0, "DS5 Bridge v0.6.0");
|
|
draw_icon(120, 0, connected ? kIconLinkOn : kIconLinkOff, 8, 8);
|
|
|
|
if (connected) {
|
|
uint8_t a[6];
|
|
bt_get_addr(a);
|
|
char buf[24];
|
|
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
|
|
a[0], a[1], a[2], a[3], a[4], a[5]);
|
|
draw_text(0, 9, buf);
|
|
|
|
const uint8_t pwr = interrupt_in_data[52];
|
|
int pct = (pwr & 0x0F) * 10;
|
|
if (pct > 100) pct = 100;
|
|
const uint8_t pstate = pwr >> 4;
|
|
char marker = ' ';
|
|
if (pstate == 1) marker = '+'; // Charging
|
|
else if (pstate == 2) marker = '*'; // Complete
|
|
else if (pstate >= 0xA) marker = '!'; // Error
|
|
char bbuf[16];
|
|
snprintf(bbuf, sizeof(bbuf), "%3d%%%c", pct, marker);
|
|
draw_text(0, 18, bbuf);
|
|
draw_battery_icon(30, 18, pct);
|
|
|
|
rect_outline(0, 30, 32, 32);
|
|
int lx = 2 + (interrupt_in_data[0] * 27) / 255;
|
|
int ly = 32 + (interrupt_in_data[1] * 27) / 255;
|
|
rect_filled(lx - 1, ly - 1, 3, 3);
|
|
|
|
rect_outline(96, 30, 32, 32);
|
|
int rx = 98 + (interrupt_in_data[2] * 27) / 255;
|
|
int ry = 32 + (interrupt_in_data[3] * 27) / 255;
|
|
rect_filled(rx - 1, ry - 1, 3, 3);
|
|
|
|
// L2/R2 analog trigger bars (vertical, fill from bottom)
|
|
rect_outline(32, 33, 4, 29);
|
|
const int l2_fill = (interrupt_in_data[4] * 27) / 255;
|
|
if (l2_fill > 0) rect_filled(33, 61 - l2_fill, 2, l2_fill);
|
|
rect_outline(92, 33, 4, 29);
|
|
const int r2_fill = (interrupt_in_data[5] * 27) / 255;
|
|
if (r2_fill > 0) rect_filled(93, 61 - r2_fill, 2, r2_fill);
|
|
|
|
const uint8_t b7 = interrupt_in_data[7];
|
|
const uint8_t b8 = interrupt_in_data[8];
|
|
|
|
// D-pad indicator (4 directions; lit for primary + diagonals)
|
|
const int dp = b7 & 0x0F;
|
|
const bool dp_n = (dp == 7 || dp == 0 || dp == 1);
|
|
const bool dp_e = (dp == 1 || dp == 2 || dp == 3);
|
|
const bool dp_s = (dp == 3 || dp == 4 || dp == 5);
|
|
const bool dp_w = (dp == 5 || dp == 6 || dp == 7);
|
|
const int dcx = 46, dcy = 46;
|
|
auto dot = [&](int dx, int dy, bool on) {
|
|
if (on) rect_filled(dcx + dx - 2, dcy + dy - 2, 5, 5);
|
|
else rect_outline(dcx + dx - 2, dcy + dy - 2, 5, 5);
|
|
};
|
|
dot(0, -7, dp_n);
|
|
dot(7, 0, dp_e);
|
|
dot(0, 7, dp_s);
|
|
dot(-7, 0, dp_w);
|
|
|
|
const int fcx = 64, fcy = 46;
|
|
auto sq = [&](int dx, int dy, bool on) {
|
|
if (on) rect_filled(fcx + dx - 2, fcy + dy - 2, 5, 5);
|
|
else rect_outline(fcx + dx - 2, fcy + dy - 2, 5, 5);
|
|
};
|
|
// shift face buttons right so they don't collide with d-pad
|
|
const int fcx_off = 18;
|
|
sq(fcx_off + 0, -8, b7 & 0x80); // Triangle
|
|
sq(fcx_off + 8, 0, b7 & 0x40); // Circle
|
|
sq(fcx_off + 0, 8, b7 & 0x20); // Cross
|
|
sq(fcx_off - 8, 0, b7 & 0x10); // Square
|
|
|
|
if (b8 & 0x01) rect_filled(36, 30, 12, 3); else rect_outline(36, 30, 12, 3); // L1
|
|
if (b8 & 0x02) rect_filled(80, 30, 12, 3); else rect_outline(80, 30, 12, 3); // R1
|
|
} else {
|
|
draw_text(0, 14, "Pair your DualSense:");
|
|
draw_text(0, 26, "1. Hold Create + PS");
|
|
draw_text(0, 36, "2. Wait for light bar");
|
|
draw_text(0, 46, " to flash blue");
|
|
}
|
|
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_rssi() {
|
|
fb_clear();
|
|
draw_text(0, 0, "BT Signal");
|
|
if (bt_is_connected()) {
|
|
int8_t rssi = 0;
|
|
bt_get_signal_strength(&rssi);
|
|
char buf[24];
|
|
snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)rssi);
|
|
draw_text(0, 12, buf);
|
|
|
|
// Map RSSI range -90..-40 dBm to 0..100% bar
|
|
int pct = ((int)rssi + 90) * 100 / 50;
|
|
if (pct < 0) pct = 0;
|
|
if (pct > 100) pct = 100;
|
|
snprintf(buf, sizeof(buf), "Quality: %d%%", pct);
|
|
draw_text(0, 22, buf);
|
|
rect_outline(0, 34, 128, 10);
|
|
int fill = (pct * 124) / 100;
|
|
if (fill > 0) rect_filled(2, 36, fill, 6);
|
|
|
|
const char *label = "Poor";
|
|
if (rssi > -55) label = "Excellent";
|
|
else if (rssi > -65) label = "Good";
|
|
else if (rssi > -75) label = "Fair";
|
|
snprintf(buf, sizeof(buf), "Link: %s", label);
|
|
draw_text(0, 48, buf);
|
|
} else {
|
|
draw_text(0, 30, "(no controller)");
|
|
}
|
|
draw_text(0, 56, "K0=next");
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_diag() {
|
|
fb_clear();
|
|
|
|
draw_text(0, 0, "Diagnostics");
|
|
|
|
const uint32_t uptime_s = time_us_32() / 1000000u;
|
|
const uint32_t h = uptime_s / 3600u;
|
|
const uint32_t m = (uptime_s / 60u) % 60u;
|
|
const uint32_t s = uptime_s % 60u;
|
|
char buf[24];
|
|
snprintf(buf, sizeof(buf), "Up:%luh %02lum %02lus", (unsigned long)h, (unsigned long)m, (unsigned long)s);
|
|
draw_text(0, 9, buf);
|
|
|
|
// Per-second rates for the audio path counters — recompute every render.
|
|
static uint32_t prev_us_frames = 0, prev_bt_packets = 0;
|
|
static uint32_t prev_sample_us = 0;
|
|
const uint32_t now_us = time_us_32();
|
|
const uint32_t cur_us_frames = audio_usb_frames();
|
|
const uint32_t cur_bt_packets = audio_bt_packets();
|
|
uint32_t usb_rate = 0, bt_rate = 0;
|
|
if (prev_sample_us != 0 && now_us > prev_sample_us) {
|
|
const uint32_t dt_us = now_us - prev_sample_us;
|
|
if (dt_us > 0) {
|
|
usb_rate = (uint32_t)(((uint64_t)(cur_us_frames - prev_us_frames) * 1000000u) / dt_us);
|
|
bt_rate = (uint32_t)(((uint64_t)(cur_bt_packets - prev_bt_packets) * 1000000u) / dt_us);
|
|
}
|
|
}
|
|
prev_us_frames = cur_us_frames;
|
|
prev_bt_packets = cur_bt_packets;
|
|
prev_sample_us = now_us;
|
|
|
|
snprintf(buf, sizeof(buf), "USB aud %lu/s", (unsigned long)usb_rate);
|
|
draw_text(0, 18, buf);
|
|
snprintf(buf, sizeof(buf), "BT 0x32 %lu/s", (unsigned long)bt_rate);
|
|
draw_text(0, 27, buf);
|
|
snprintf(buf, sizeof(buf), "HCI errs: %lu", (unsigned long)bt_hci_err_count());
|
|
draw_text(0, 36, buf);
|
|
|
|
snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting");
|
|
draw_text(0, 45, buf);
|
|
|
|
draw_text(0, 56, "K0=next K1=back");
|
|
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_cpu(bool entered) {
|
|
fb_clear();
|
|
draw_text(0, 0, "CPU / Clock");
|
|
|
|
char buf[24];
|
|
|
|
// Configured system clock — compile-time SYS_CLOCK_KHZ, set in main()
|
|
// via set_sys_clock_khz(). This is the *target*.
|
|
const uint32_t set_khz = (uint32_t)SYS_CLOCK_KHZ;
|
|
snprintf(buf, sizeof(buf), "Set : %lu MHz", (unsigned long)(set_khz / 1000u));
|
|
draw_text(0, 12, buf);
|
|
|
|
// Actually running clk_sys, measured by the on-chip frequency counter
|
|
// against the crystal reference (not just what we asked for). The counter
|
|
// busy-waits a few ms per call, so measure ONCE on screen entry and cache
|
|
// it — clk_sys is fixed at boot and never changes, so the temperature
|
|
// (which legitimately drifts) is the only thing worth refreshing per
|
|
// frame. cached_real_khz==0 also forces a (re)measure as a safety net.
|
|
static uint32_t cached_real_khz = 0;
|
|
if (entered || cached_real_khz == 0) {
|
|
cached_real_khz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS);
|
|
}
|
|
const uint32_t real_khz = cached_real_khz;
|
|
snprintf(buf, sizeof(buf), "Real: %lu.%01lu MHz",
|
|
(unsigned long)(real_khz / 1000u),
|
|
(unsigned long)((real_khz % 1000u) / 100u));
|
|
draw_text(0, 22, buf);
|
|
|
|
// Core voltage actually programmed into the regulator, read back (not the
|
|
// compile-time constant). Codes 0..15 are linear 0.05 V steps from 0.55 V.
|
|
const int vcode = (int)vreg_get_voltage();
|
|
if (vcode >= 0 && vcode <= 0b01111) {
|
|
const unsigned mv = 550u + 50u * (unsigned)vcode;
|
|
snprintf(buf, sizeof(buf), "Vcore: %u.%02u V", mv / 1000u, (mv % 1000u) / 10u);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "Vcore: code %d", vcode);
|
|
}
|
|
draw_text(0, 32, buf);
|
|
|
|
// RP2350 on-die temperature sensor. Smoothed + averaged in cmd.cpp
|
|
// (single source of truth shared with the 0xfc web telemetry) so the
|
|
// reading converges to the true die temp instead of chasing ADC noise.
|
|
const uint16_t raw = cpu_temp_raw_smoothed();
|
|
const float volts = (float)raw * 3.3f / 4096.0f;
|
|
const float temp_c = 27.0f - (volts - 0.706f) / 0.001721f;
|
|
const int t10 = (int)(temp_c * 10.0f + (temp_c >= 0 ? 0.5f : -0.5f));
|
|
snprintf(buf, sizeof(buf), "Temp : %d.%d C", t10 / 10,
|
|
(t10 < 0 ? -t10 : t10) % 10);
|
|
draw_text(0, 42, buf);
|
|
|
|
draw_text(0, 56, "K0=next K1=back");
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_triggers() {
|
|
fb_clear();
|
|
draw_text(0, 0, "Trigger Test");
|
|
|
|
char buf[24];
|
|
snprintf(buf, sizeof(buf), "Mode: %s", kTrigPresetNames[trigger_preset]);
|
|
draw_text(0, 12, buf);
|
|
|
|
if (bt_is_connected()) {
|
|
const uint8_t l2 = interrupt_in_data[4];
|
|
const uint8_t r2 = interrupt_in_data[5];
|
|
snprintf(buf, sizeof(buf), "L2:%3d R2:%3d", l2, r2);
|
|
draw_text(0, 24, buf);
|
|
|
|
rect_outline(0, 35, 60, 9);
|
|
int lfill = (l2 * 56) / 255;
|
|
if (lfill > 0) rect_filled(2, 37, lfill, 5);
|
|
rect_outline(68, 35, 60, 9);
|
|
int rfill = (r2 * 56) / 255;
|
|
if (rfill > 0) rect_filled(70, 37, rfill, 5);
|
|
} else {
|
|
draw_text(0, 24, "(no controller)");
|
|
}
|
|
|
|
draw_text(0, 56, "K0=next K1=cycle");
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_gyro() {
|
|
fb_clear();
|
|
draw_text(0, 0, "Gyro Tilt");
|
|
if (bt_is_connected()) {
|
|
int16_t ax, ay, az;
|
|
memcpy(&ax, &interrupt_in_data[21], 2);
|
|
memcpy(&ay, &interrupt_in_data[23], 2);
|
|
memcpy(&az, &interrupt_in_data[25], 2);
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "X%+5d", ax); draw_text(0, 10, buf);
|
|
snprintf(buf, sizeof(buf), "Y%+5d", ay); draw_text(44, 10, buf);
|
|
snprintf(buf, sizeof(buf), "Z%+5d", az); draw_text(88, 10, buf);
|
|
|
|
const int bx = 44, by = 22, bw = 40, bh = 40;
|
|
rect_outline(bx, by, bw, bh);
|
|
for (int x = bx + 1; x < bx + bw - 1; x++) px(x, by + bh / 2, true);
|
|
for (int y = by + 1; y < by + bh - 1; y++) px(bx + bw / 2, y, true);
|
|
int dx = ((int)ax * (bw / 2 - 3)) / 8192;
|
|
int dy = ((int)ay * (bh / 2 - 3)) / 8192;
|
|
int cx = bx + bw / 2 + dx;
|
|
int cy = by + bh / 2 + dy;
|
|
if (cx < bx + 2) cx = bx + 2;
|
|
if (cx > bx + bw - 3) cx = bx + bw - 3;
|
|
if (cy < by + 2) cy = by + 2;
|
|
if (cy > by + bh - 3) cy = by + bh - 3;
|
|
rect_filled(cx - 1, cy - 1, 3, 3);
|
|
} else {
|
|
draw_text(0, 30, "(no controller)");
|
|
}
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_touchpad() {
|
|
fb_clear();
|
|
draw_text(0, 0, "Touchpad");
|
|
if (bt_is_connected()) {
|
|
rect_outline(4, 12, 120, 30);
|
|
int active = 0;
|
|
for (int finger = 0; finger < 2; finger++) {
|
|
const int off = 32 + finger * 4;
|
|
const uint32_t f = (uint32_t)interrupt_in_data[off] |
|
|
((uint32_t)interrupt_in_data[off + 1] << 8) |
|
|
((uint32_t)interrupt_in_data[off + 2] << 16) |
|
|
((uint32_t)interrupt_in_data[off + 3] << 24);
|
|
const bool not_touching = (f >> 7) & 1u;
|
|
if (not_touching) continue;
|
|
const uint16_t fx = (f >> 8) & 0xFFFu;
|
|
const uint16_t fy = (f >> 20) & 0xFFFu;
|
|
int sx = 5 + ((int)fx * 114) / 1919;
|
|
int sy = 13 + ((int)fy * 26) / 1079;
|
|
if (sx < 5) sx = 5;
|
|
if (sx > 122) sx = 122;
|
|
if (sy < 13) sy = 13;
|
|
if (sy > 40) sy = 40;
|
|
rect_filled(sx - 1, sy - 1, 3, 3);
|
|
active++;
|
|
}
|
|
char buf[20];
|
|
snprintf(buf, sizeof(buf), "Fingers: %d", active);
|
|
draw_text(0, 46, buf);
|
|
} else {
|
|
draw_text(0, 30, "(no controller)");
|
|
}
|
|
draw_text(0, 56, "K0=next");
|
|
flush_fb();
|
|
}
|
|
|
|
void send_lightbar_color(uint8_t r, uint8_t g, uint8_t b) {
|
|
uint8_t pkt[78] = {};
|
|
pkt[0] = 0x31;
|
|
pkt[2] = 0x10;
|
|
pkt[4] = 0x04; // valid_flag1: LIGHTBAR_CONTROL_ENABLE (bit 2)
|
|
pkt[47] = r; // lightbar_red
|
|
pkt[48] = g; // lightbar_green
|
|
pkt[49] = b; // lightbar_blue
|
|
bt_write(pkt, sizeof(pkt));
|
|
}
|
|
|
|
// Tiny 32-step sine LUT (no <cmath>). angle 0..255 → amplitude -127..127.
|
|
static const int8_t kSine32[32] = {
|
|
0, 24, 49, 70, 90, 106, 117, 125, 127, 125, 117, 106, 90, 70, 49, 24,
|
|
0, -24, -49, -70, -90, -106, -117, -125, -127, -125, -117, -106, -90, -70, -49, -24,
|
|
};
|
|
int sin_lut(uint8_t a) { return kSine32[(a >> 3) & 0x1F]; }
|
|
|
|
void hsv_to_rgb(uint16_t h, uint8_t s, uint8_t v, uint8_t *r, uint8_t *g, uint8_t *b) {
|
|
if (h >= 360) h %= 360;
|
|
const uint8_t region = (uint8_t)(h / 60);
|
|
const uint16_t remainder = (uint16_t)((h - region * 60u) * 256u / 60u);
|
|
const uint8_t p = (uint8_t)(((uint16_t)v * (255u - s)) >> 8);
|
|
const uint8_t q = (uint8_t)(((uint16_t)v * (255u - (((uint16_t)s * remainder) >> 8))) >> 8);
|
|
const uint8_t t = (uint8_t)(((uint16_t)v * (255u - (((uint16_t)s * (255u - remainder)) >> 8))) >> 8);
|
|
switch (region) {
|
|
case 0: *r = v; *g = t; *b = p; break;
|
|
case 1: *r = q; *g = v; *b = p; break;
|
|
case 2: *r = p; *g = v; *b = t; break;
|
|
case 3: *r = p; *g = q; *b = v; break;
|
|
case 4: *r = t; *g = p; *b = v; break;
|
|
default: *r = v; *g = p; *b = q; break;
|
|
}
|
|
}
|
|
|
|
const char* lb_mode_tag(int mode) {
|
|
switch (mode) {
|
|
case 0: return "[LIVE]";
|
|
case 1: return "[FAV0]";
|
|
case 2: return "[FAV1]";
|
|
case 3: return "[FAV2]";
|
|
case 4: return "[FAV3]";
|
|
case 5: return "[BREA]";
|
|
case 6: return "[RAIN]";
|
|
case 7: return "[FADE]";
|
|
default: return "[????]";
|
|
}
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_lightbar() {
|
|
fb_clear();
|
|
draw_text(0, 0, "Lightbar");
|
|
draw_text(86, 0, lb_mode_tag(lb_mode));
|
|
|
|
if (bt_is_connected()) {
|
|
const uint32_t now_ms = time_us_32() / 1000;
|
|
if (lb_mode == 0) {
|
|
// LIVE: tilt -> RGB
|
|
int16_t ax, ay, az;
|
|
memcpy(&ax, &interrupt_in_data[21], 2);
|
|
memcpy(&ay, &interrupt_in_data[23], 2);
|
|
memcpy(&az, &interrupt_in_data[25], 2);
|
|
const int rr = ((int)ax + 8192) * 255 / 16384;
|
|
const int gg = ((int)ay + 8192) * 255 / 16384;
|
|
const int bb = ((int)az + 8192) * 255 / 16384;
|
|
lb_r = (uint8_t)(rr < 0 ? 0 : rr > 255 ? 255 : rr);
|
|
lb_g = (uint8_t)(gg < 0 ? 0 : gg > 255 ? 255 : gg);
|
|
lb_b = (uint8_t)(bb < 0 ? 0 : bb > 255 ? 255 : bb);
|
|
} else if (lb_mode <= 4) {
|
|
// FAV slot: fixed color
|
|
const int slot = lb_mode - 1;
|
|
lb_r = lb_fav_r[slot];
|
|
lb_g = lb_fav_g[slot];
|
|
lb_b = lb_fav_b[slot];
|
|
} else if (lb_mode == 5) {
|
|
// BREATHING: modulate FAV0 brightness with a sine wave (~3 s cycle)
|
|
const uint8_t phase = (uint8_t)(now_ms / 12);
|
|
const int s = sin_lut(phase); // -127..127
|
|
const uint16_t scale = (uint16_t)(32 + (s + 127) / 2); // 32..191
|
|
lb_r = (uint8_t)((lb_fav_r[0] * scale) / 255);
|
|
lb_g = (uint8_t)((lb_fav_g[0] * scale) / 255);
|
|
lb_b = (uint8_t)((lb_fav_b[0] * scale) / 255);
|
|
} else if (lb_mode == 6) {
|
|
// RAINBOW: hue sweep over ~6 s
|
|
const uint16_t hue = (uint16_t)((now_ms / 17) % 360);
|
|
hsv_to_rgb(hue, 255, 255, &lb_r, &lb_g, &lb_b);
|
|
} else {
|
|
// FADE between FAV slots, 2 s per slot
|
|
const uint32_t kSlotMs = 2000;
|
|
const uint32_t total = now_ms % (4 * kSlotMs);
|
|
const int slot = (int)(total / kSlotMs);
|
|
const int next = (slot + 1) & 3;
|
|
const uint16_t blend = (uint16_t)(((total - slot * kSlotMs) * 256u) / kSlotMs);
|
|
lb_r = (uint8_t)((lb_fav_r[slot] * (255 - blend) + lb_fav_r[next] * blend) / 255);
|
|
lb_g = (uint8_t)((lb_fav_g[slot] * (255 - blend) + lb_fav_g[next] * blend) / 255);
|
|
lb_b = (uint8_t)((lb_fav_b[slot] * (255 - blend) + lb_fav_b[next] * blend) / 255);
|
|
}
|
|
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "R:%3u", lb_r); draw_text(0, 12, buf);
|
|
snprintf(buf, sizeof(buf), "G:%3u", lb_g); draw_text(44, 12, buf);
|
|
snprintf(buf, sizeof(buf), "B:%3u", lb_b); draw_text(88, 12, buf);
|
|
|
|
const int by = 22, bh = 8;
|
|
rect_outline(0, by, 40, bh); int rf = (lb_r * 36) / 255; if (rf > 0) rect_filled(2, by + 2, rf, bh - 4);
|
|
rect_outline(44, by, 40, bh); int gf = (lb_g * 36) / 255; if (gf > 0) rect_filled(46, by + 2, gf, bh - 4);
|
|
rect_outline(88, by, 40, bh); int bf = (lb_b * 36) / 255; if (bf > 0) rect_filled(90, by + 2, bf, bh - 4);
|
|
|
|
// Face button rising-edge -> save current color to slot 0..3
|
|
const uint8_t face = interrupt_in_data[7] & 0xF0;
|
|
const uint8_t pressed = face & ~lb_last_face;
|
|
lb_last_face = face;
|
|
int save_slot = -1;
|
|
if (pressed & 0x80) save_slot = 0; // Triangle
|
|
else if (pressed & 0x40) save_slot = 1; // Circle
|
|
else if (pressed & 0x20) save_slot = 2; // Cross
|
|
else if (pressed & 0x10) save_slot = 3; // Square
|
|
if (save_slot >= 0) {
|
|
lb_fav_r[save_slot] = lb_r;
|
|
lb_fav_g[save_slot] = lb_g;
|
|
lb_fav_b[save_slot] = lb_b;
|
|
}
|
|
|
|
draw_text(0, 38, "Sv:T=0 C=1 X=2 S=3");
|
|
const char* hint =
|
|
(lb_mode == 0) ? "Tilt = R/G/B" :
|
|
(lb_mode == 5) ? "Breathing FAV0" :
|
|
(lb_mode == 6) ? "Rainbow sweep" :
|
|
(lb_mode == 7) ? "Fade thru FAVs" :
|
|
"Locked to fav";
|
|
draw_text(0, 48, hint);
|
|
|
|
send_lightbar_color(lb_r, lb_g, lb_b);
|
|
} else {
|
|
draw_text(0, 30, "(no controller)");
|
|
}
|
|
draw_text(0, 56, "K0=next K1=cycle");
|
|
flush_fb();
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_vu() {
|
|
fb_clear();
|
|
draw_text(0, 0, "Audio Meters");
|
|
if (bt_is_connected()) {
|
|
const uint8_t spk = audio_peak_speaker();
|
|
const uint8_t hap = audio_peak_haptic();
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "SPK %3u", spk);
|
|
draw_text(0, 14, buf);
|
|
rect_outline(48, 14, 80, 8);
|
|
int sfill = (spk * 76) / 255;
|
|
if (sfill > 0) rect_filled(50, 16, sfill, 4);
|
|
|
|
snprintf(buf, sizeof(buf), "HAP %3u", hap);
|
|
draw_text(0, 28, buf);
|
|
rect_outline(48, 28, 80, 8);
|
|
int hfill = (hap * 76) / 255;
|
|
if (hfill > 0) rect_filled(50, 30, hfill, 4);
|
|
|
|
draw_text(0, 42, "Live USB audio peaks");
|
|
} else {
|
|
draw_text(0, 30, "(no controller)");
|
|
}
|
|
draw_text(0, 56, "K0=next");
|
|
flush_fb();
|
|
}
|
|
|
|
void settings_adjust(int delta) {
|
|
Config_body &c = settings_local;
|
|
settings_dirty = true;
|
|
switch (settings_sel) {
|
|
case 0: { // haptics_gain [1.0, 2.0] step 0.1
|
|
int v = (int)(c.haptics_gain * 10.0f + 0.5f) + delta;
|
|
if (v < 10) v = 10; if (v > 20) v = 20;
|
|
c.haptics_gain = v / 10.0f;
|
|
break;
|
|
}
|
|
case 1: { // speaker_volume [-100, 0] step 5
|
|
int v = (int)c.speaker_volume + delta * 5;
|
|
if (v < -100) v = -100; if (v > 0) v = 0;
|
|
c.speaker_volume = (float)v;
|
|
break;
|
|
}
|
|
case 2: { // inactive_time [10, 60] step 5
|
|
int v = (int)c.inactive_time + delta * 5;
|
|
if (v < 10) v = 10; if (v > 60) v = 60;
|
|
c.inactive_time = (uint8_t)v;
|
|
break;
|
|
}
|
|
case 3: c.disable_inactive_disconnect ^= 1; break;
|
|
case 4: c.disable_pico_led ^= 1; break;
|
|
case 5: { // polling_rate_mode 0..2
|
|
int v = (int)c.polling_rate_mode + delta;
|
|
if (v < 0) v = 2; if (v > 2) v = 0;
|
|
c.polling_rate_mode = (uint8_t)v;
|
|
break;
|
|
}
|
|
case 6: { // audio_buffer_length [16, 128] step 4
|
|
int v = (int)c.audio_buffer_length + delta * 4;
|
|
if (v < 16) v = 16; if (v > 128) v = 128;
|
|
c.audio_buffer_length = (uint8_t)v;
|
|
break;
|
|
}
|
|
case 7: { // controller_mode 0..2
|
|
int v = (int)c.controller_mode + delta;
|
|
if (v < 0) v = 2; if (v > 2) v = 0;
|
|
c.controller_mode = (uint8_t)v;
|
|
break;
|
|
}
|
|
case 8: { // auto_haptics_enable 0..3
|
|
int v = (int)c.auto_haptics_enable + delta;
|
|
if (v < 0) v = 3; if (v > 3) v = 0;
|
|
c.auto_haptics_enable = (uint8_t)v;
|
|
break;
|
|
}
|
|
case 9: { // auto_haptics_gain [0, 200] step 10
|
|
int v = (int)c.auto_haptics_gain + delta * 10;
|
|
if (v < 0) v = 0; if (v > 200) v = 200;
|
|
c.auto_haptics_gain = (uint8_t)v;
|
|
break;
|
|
}
|
|
case 10: { // auto_haptics_lowpass 0..3
|
|
int v = (int)c.auto_haptics_lowpass + delta;
|
|
if (v < 0) v = 3; if (v > 3) v = 0;
|
|
c.auto_haptics_lowpass = (uint8_t)v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void settings_handle_input() {
|
|
if (!bt_is_connected()) return;
|
|
const uint8_t dpad = (uint8_t)(interrupt_in_data[7] & 0x0F);
|
|
const uint8_t face = (uint8_t)(interrupt_in_data[7] & 0xF0);
|
|
|
|
// Edge-trigger on D-pad direction CHANGE; only pure N/E/S/W to avoid diagonals
|
|
if (dpad != settings_last_dpad && dpad != 8) {
|
|
if (dpad == 0) settings_sel = (settings_sel - 1 + kNumSettingsItems) % kNumSettingsItems;
|
|
else if (dpad == 4) settings_sel = (settings_sel + 1) % kNumSettingsItems;
|
|
else if (dpad == 6) settings_adjust(-1);
|
|
else if (dpad == 2) settings_adjust(+1);
|
|
}
|
|
settings_last_dpad = dpad;
|
|
|
|
// Triangle handling — Reset and Wipe-slots items both require a 2 s hold;
|
|
// every other item saves edits on a normal short press.
|
|
const bool tri_now = (face & 0x80) != 0;
|
|
const bool tri_prev = (settings_last_face & 0x80) != 0;
|
|
const bool is_hold_item = (settings_sel == kSettingsResetIdx
|
|
|| settings_sel == kSettingsWipeSlotsIdx);
|
|
if (tri_now && !tri_prev) {
|
|
settings_tri_press_us = (uint32_t)time_us_32();
|
|
settings_reset_triggered = false;
|
|
}
|
|
if (is_hold_item && tri_now && !settings_reset_triggered
|
|
&& ((uint32_t)time_us_32() - settings_tri_press_us) >= kResetHoldUs) {
|
|
settings_reset_triggered = true;
|
|
if (settings_sel == kSettingsResetIdx) {
|
|
config_default();
|
|
if (config_save()) {
|
|
settings_local = get_config();
|
|
settings_dirty = false;
|
|
settings_save_status = "Reset!";
|
|
} else {
|
|
settings_save_status = "Reset FAIL";
|
|
}
|
|
} else {
|
|
bt_wipe_all_slots();
|
|
settings_save_status = "Slots wiped!";
|
|
}
|
|
}
|
|
if (!tri_now && tri_prev) {
|
|
if (!is_hold_item && !settings_reset_triggered) {
|
|
set_config(settings_local);
|
|
settings_save_status = config_save() ? "Saved!" : "Save FAIL";
|
|
if (settings_save_status[0] == 'S' && settings_save_status[1] == 'a') {
|
|
settings_dirty = false;
|
|
}
|
|
}
|
|
settings_reset_triggered = false;
|
|
}
|
|
settings_last_face = face;
|
|
}
|
|
|
|
__attribute__((noinline)) void format_settings_item(int idx, char* line, size_t n) {
|
|
const Config_body &c = settings_local;
|
|
const char *cur = (idx == settings_sel) ? ">" : " ";
|
|
switch (idx) {
|
|
case 0: {
|
|
int g = (int)(c.haptics_gain * 10.0f + 0.5f);
|
|
snprintf(line, n, "%s Hap Gain %d.%dx", cur, g / 10, g % 10);
|
|
break;
|
|
}
|
|
case 1: snprintf(line, n, "%s Spk Vol %ddB", cur, (int)c.speaker_volume); break;
|
|
case 2: snprintf(line, n, "%s Inact %umin", cur, c.inactive_time); break;
|
|
case 3: snprintf(line, n, "%s InactDC %s", cur, c.disable_inactive_disconnect ? "off" : "on"); break;
|
|
case 4: snprintf(line, n, "%s Pico LED %s", cur, c.disable_pico_led ? "off" : "on"); break;
|
|
case 5: {
|
|
const char* names[3] = {"250Hz", "500Hz", "RT"};
|
|
snprintf(line, n, "%s Poll %s", cur, names[c.polling_rate_mode % 3]);
|
|
break;
|
|
}
|
|
case 6: snprintf(line, n, "%s AudBuf %u", cur, c.audio_buffer_length); break;
|
|
case 7: {
|
|
const char* names[3] = {"DS5", "DSE", "Auto"};
|
|
snprintf(line, n, "%s Ctrl %s", cur, names[c.controller_mode % 3]);
|
|
break;
|
|
}
|
|
case 8: {
|
|
const char* names[4] = {"Off", "Fallback", "Mix", "Replace"};
|
|
snprintf(line, n, "%s AutoHap %s", cur, names[c.auto_haptics_enable & 3]);
|
|
break;
|
|
}
|
|
case 9: snprintf(line, n, "%s AH Gain %u%%", cur, c.auto_haptics_gain); break;
|
|
case 10: {
|
|
const char* names[4] = {"80Hz", "160Hz", "250Hz", "400Hz"};
|
|
snprintf(line, n, "%s AH LP %s", cur, names[c.auto_haptics_lowpass & 3]);
|
|
break;
|
|
}
|
|
case 11: snprintf(line, n, "%s Reset to defaults", cur); break;
|
|
case 12: snprintf(line, n, "%s Wipe all slots", cur); break;
|
|
}
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_settings() {
|
|
if (!settings_init_done) {
|
|
settings_local = get_config();
|
|
settings_init_done = true;
|
|
}
|
|
settings_handle_input();
|
|
|
|
fb_clear();
|
|
char buf[24];
|
|
snprintf(buf, sizeof(buf), "Settings %s", settings_dirty ? "(*)" : " ");
|
|
draw_text(0, 0, buf);
|
|
if (settings_save_status[0]) {
|
|
draw_text(86, 0, settings_save_status);
|
|
}
|
|
|
|
constexpr int kVisible = 5;
|
|
int top = 0;
|
|
if (settings_sel >= kVisible) top = settings_sel - kVisible + 1;
|
|
char line[28];
|
|
for (int i = 0; i < kVisible && top + i < kNumSettingsItems; i++) {
|
|
format_settings_item(top + i, line, sizeof(line));
|
|
draw_text(0, 9 + i * 9, line);
|
|
}
|
|
|
|
if (settings_sel == kSettingsResetIdx) {
|
|
draw_text(0, 56, "Hold Tri 2s = RESET");
|
|
} else if (settings_sel == kSettingsWipeSlotsIdx) {
|
|
draw_text(0, 56, "Hold Tri 2s = WIPE");
|
|
} else {
|
|
draw_text(0, 56, "DP nav/adj Tri=save");
|
|
}
|
|
flush_fb();
|
|
}
|
|
|
|
// ---- Slots screen (Phase G) ----------------------------------------------
|
|
// Multi-slot persistent pairing UI. Modeled on zurce/DS5Dongle-OLED.
|
|
// Credit to zurce.
|
|
|
|
int slots_cursor = -1; // initialized to active slot on first entry
|
|
uint8_t slots_last_dpad = 8;
|
|
uint8_t slots_last_face = 0;
|
|
uint32_t slots_sq_press_us = 0;
|
|
bool slots_wipe_triggered = false;
|
|
const char* slots_status = "";
|
|
uint32_t slots_status_until_us = 0;
|
|
constexpr uint32_t kSlotsWipeHoldUs = 1500000; // 1.5 s
|
|
|
|
void slots_handle_input() {
|
|
if (slots_cursor < 0) slots_cursor = bt_get_slot();
|
|
if (!bt_is_connected()) {
|
|
// Even without a DS5 connected we still want to navigate / wipe;
|
|
// we just can't read the controller's D-pad / face inputs. Return
|
|
// here and require KEY0/KEY1 for screen switching.
|
|
slots_last_dpad = 8;
|
|
slots_last_face = 0;
|
|
return;
|
|
}
|
|
const uint8_t dpad = (uint8_t)(interrupt_in_data[7] & 0x0F);
|
|
const uint8_t face = (uint8_t)(interrupt_in_data[7] & 0xF0);
|
|
|
|
if (dpad != slots_last_dpad && dpad != 8) {
|
|
if (dpad == 0) slots_cursor = (slots_cursor - 1 + kNumSlots) % kNumSlots;
|
|
else if (dpad == 4) slots_cursor = (slots_cursor + 1) % kNumSlots;
|
|
}
|
|
slots_last_dpad = dpad;
|
|
|
|
// Triangle rising edge: switch to cursor slot if different from active
|
|
const bool tri_now = (face & 0x80) != 0;
|
|
const bool tri_prev = (slots_last_face & 0x80) != 0;
|
|
if (tri_now && !tri_prev) {
|
|
if (slots_cursor != bt_get_slot()) {
|
|
bt_set_slot(slots_cursor);
|
|
slots_status = "Switched!";
|
|
slots_status_until_us = (uint32_t)time_us_32() + 1500000;
|
|
}
|
|
}
|
|
|
|
// Square hold 1.5 s: wipe cursor slot
|
|
const bool sq_now = (face & 0x10) != 0;
|
|
const bool sq_prev = (slots_last_face & 0x10) != 0;
|
|
if (sq_now && !sq_prev) {
|
|
slots_sq_press_us = (uint32_t)time_us_32();
|
|
slots_wipe_triggered = false;
|
|
}
|
|
if (sq_now && !slots_wipe_triggered
|
|
&& ((uint32_t)time_us_32() - slots_sq_press_us) >= kSlotsWipeHoldUs) {
|
|
slots_wipe_triggered = true;
|
|
bt_forget_slot(slots_cursor);
|
|
slots_status = "Wiped!";
|
|
slots_status_until_us = (uint32_t)time_us_32() + 1500000;
|
|
}
|
|
if (!sq_now && sq_prev) slots_wipe_triggered = false;
|
|
|
|
slots_last_face = face;
|
|
}
|
|
|
|
__attribute__((noinline)) void render_screen_slots() {
|
|
slots_handle_input();
|
|
if (slots_cursor < 0) slots_cursor = bt_get_slot();
|
|
|
|
fb_clear();
|
|
char hdr[24];
|
|
const int active = bt_get_slot();
|
|
const bool conn = bt_is_connected();
|
|
snprintf(hdr, sizeof(hdr), "Slots [s%d %s]", active, conn ? "ON" : "--");
|
|
draw_text(0, 0, hdr);
|
|
|
|
if (slots_status[0] && (uint32_t)time_us_32() < slots_status_until_us) {
|
|
draw_text(80, 0, slots_status);
|
|
}
|
|
|
|
for (int i = 0; i < kNumSlots; i++) {
|
|
char line[28];
|
|
const char *cursor_mark = (i == slots_cursor) ? ">" : " ";
|
|
const char *active_mark = (i == active) ? "*" : " ";
|
|
if (slot_occupied(i)) {
|
|
uint8_t a[6];
|
|
slot_get_addr(i, a);
|
|
snprintf(line, sizeof(line), "%s%d%s %02X:%02X:%02X:%02X:%02X:%02X",
|
|
cursor_mark, i, active_mark, a[0], a[1], a[2], a[3], a[4], a[5]);
|
|
} else {
|
|
snprintf(line, sizeof(line), "%s%d%s (empty)", cursor_mark, i, active_mark);
|
|
}
|
|
draw_text(0, 9 + i * 9, line);
|
|
}
|
|
|
|
draw_text(0, 56, "Tri=switch Sq hold=wipe");
|
|
flush_fb();
|
|
}
|
|
|
|
void boot_splash() {
|
|
fb_clear();
|
|
auto cx_for = [](const char* s) {
|
|
int n = 0; while (s[n]) n++;
|
|
return (128 - (n * 6 - 1)) / 2;
|
|
};
|
|
const char* l1 = "DS5 Bridge";
|
|
const char* l2 = "v0.6.0";
|
|
const char* l3 = "Pico2W + OLED";
|
|
draw_text(cx_for(l1), 16, l1);
|
|
draw_text(cx_for(l2), 30, l2);
|
|
draw_text(cx_for(l3), 44, l3);
|
|
flush_fb();
|
|
sleep_ms(1500);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void oled_init() {
|
|
spi_init(spi1, 10 * 1000 * 1000);
|
|
gpio_set_function(kPinCLK, GPIO_FUNC_SPI);
|
|
gpio_set_function(kPinMOSI, GPIO_FUNC_SPI);
|
|
|
|
gpio_init(kPinCS); gpio_set_dir(kPinCS, GPIO_OUT); gpio_put(kPinCS, 1);
|
|
gpio_init(kPinDC); gpio_set_dir(kPinDC, GPIO_OUT); gpio_put(kPinDC, 0);
|
|
gpio_init(kPinRST); gpio_set_dir(kPinRST, GPIO_OUT); gpio_put(kPinRST, 1);
|
|
|
|
gpio_init(kPinKey0); gpio_set_dir(kPinKey0, GPIO_IN); gpio_pull_up(kPinKey0);
|
|
gpio_init(kPinKey1); gpio_set_dir(kPinKey1, GPIO_IN); gpio_pull_up(kPinKey1);
|
|
|
|
hw_reset();
|
|
sh1107_init();
|
|
fb_clear();
|
|
boot_splash();
|
|
}
|
|
|
|
void oled_loop() {
|
|
handle_buttons();
|
|
const uint32_t now = time_us_32();
|
|
rumble_burst_tick(now);
|
|
if ((now - last_render_us) < kFrameUs) return;
|
|
last_render_us = now;
|
|
// Bump activity on controller input changes (cheap rolling hash over input bytes)
|
|
uint32_t hash = 0;
|
|
for (int i = 0; i < 10; i++) hash = hash * 31u + interrupt_in_data[i];
|
|
if (hash != last_input_hash) {
|
|
last_input_hash = hash;
|
|
last_activity_us = now;
|
|
}
|
|
|
|
// Auto-dim after idle; respect user-selected brightness otherwise
|
|
const bool idle = (now - last_activity_us) > kAutoDimUs;
|
|
sh1107_set_contrast(idle ? kDimContrast : kBrightLevels[bright_idx]);
|
|
|
|
// True on the first render after navigating to a different screen.
|
|
// Lets a screen do expensive one-shot work on entry (the CPU screen
|
|
// caches its frequency-counter measurement here).
|
|
static int last_rendered_screen = -1;
|
|
const bool screen_entered = (current_screen != last_rendered_screen);
|
|
last_rendered_screen = current_screen;
|
|
|
|
switch (current_screen) {
|
|
case kScreenStatus: render_screen(); break;
|
|
case kScreenSlots: render_screen_slots(); break;
|
|
case kScreenLightbar: render_screen_lightbar(); break;
|
|
case kScreenTriggers: render_screen_triggers(); break;
|
|
case kScreenGyro: render_screen_gyro(); break;
|
|
case kScreenTouchpad: render_screen_touchpad(); break;
|
|
case kScreenDiag: render_screen_diag(); break;
|
|
case kScreenCpu: render_screen_cpu(screen_entered); break;
|
|
case kScreenRssi: render_screen_rssi(); break;
|
|
case kScreenVU: render_screen_vu(); break;
|
|
case kScreenSettings: render_screen_settings(); break;
|
|
}
|
|
}
|