/*
CrowPanel 1.28" (ESP32-S3 + GC9A01, TFT_eSPI)
Outer dial: glued labels, reversed order, −30 offset; world-grid ticks; smooth big-step tween.
Inner dial: world-grid ticks + labels every 20th minor (60°) showing MHz with one decimal,
computed from the actual frequency at that angle (matches the generator).
by mircemk November 2025
*/
#include <Arduino.h>
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
#include <si5351.h>
#include "CST816D.h"
//#include <driver/ledc.h>
#include <Adafruit_NeoPixel.h>
#define LED_PIN 48
#define LED_COUNT 5
#define LED_BRIGHTNESS 0
Adafruit_NeoPixel ring(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
int ledPos = 0; // current LED index
#define UPPER_DIR (-1) // +1 = left→right increasing; -1 = reversed
// --- Display power / init ---
#define USE_PANEL_ENABLE_PINS 1
#define PIN_LCD_PWR_EN1 1
#define PIN_LCD_PWR_EN2 2
#define PIN_TFT_BL 46
#define PIN_TFT_RST 14
#define BL_CHANNEL 0
#define BL_FREQ 2000
#define BL_RES_BITS 8
#define FLASH_TIME_MS 200
// --- CrowPanel Touch Pins ---
#define TP_I2C_SDA_PIN 6
#define TP_I2C_SCL_PIN 7
#define TP_RST 13
#define TP_INT 5
// --- Create Touch Object ---
CST816D touch(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN, TP_RST, TP_INT);
TFT_eSPI tft;
static void panelPowerOn(){ if(USE_PANEL_ENABLE_PINS){ pinMode(PIN_LCD_PWR_EN1,OUTPUT); pinMode(PIN_LCD_PWR_EN2,OUTPUT); digitalWrite(PIN_LCD_PWR_EN1,HIGH); digitalWrite(PIN_LCD_PWR_EN2,HIGH); delay(5);} }
static void pulseResetPin(){ pinMode(PIN_TFT_RST,OUTPUT); digitalWrite(PIN_TFT_RST,HIGH); delay(5); digitalWrite(PIN_TFT_RST,LOW); delay(10); digitalWrite(PIN_TFT_RST,HIGH); delay(20); }
static void backlightInit(uint8_t duty){ pinMode(PIN_TFT_BL,OUTPUT); ledcSetup(BL_CHANNEL,BL_FREQ,BL_RES_BITS); ledcAttachPin(PIN_TFT_BL,BL_CHANNEL); ledcWrite(BL_CHANNEL,duty); }
// --- IO pins ---
#define ENC_A 45
#define ENC_B 42
#define ENC_BTN 41
#define SI5351_SDA 38
#define SI5351_SCL 39
uint32_t colors[] = {
ring.Color(0, 0, 255), // blue
ring.Color(0, 255, 255), // cyan
ring.Color(255, 0, 255), // magenta
ring.Color(255, 255, 0) // yellow
};
// --- VFO / Si5351 ---
static const uint64_t FREQ_MIN=10000ULL, FREQ_MAX=160000000ULL, FREQ_INIT=10100000ULL;
static const int32_t SI5351_CORR_PPM=0;
uint32_t stepLadder[]={10,100,1000,10000,100000,1000000};
uint8_t stepIndex=2; // 1 kHz
uint64_t vfoHz=FREQ_INIT;
Si5351 si5351;
// ---------- Bands ----------
struct BandInfo {
const char* name;
uint64_t startFreq;
const char* wavelength;
};
BandInfo bands[] = {
{"LW", 148500ULL, "2010 m"},
{"MW", 520000ULL, "577 m"},
{"SW 1", 1800000ULL, "160 m"},
{"SW 2", 3500000ULL, "80 m"},
{"SW 3", 5000000ULL, "60 m"},
{"SW 4", 7000000ULL, "40 m"},
{"SW 5", 10100000ULL, "30 m"},
{"SW 6", 14000000ULL, "20 m"},
{"SW 7", 18000000ULL, "17 m"},
{"SW 8", 21000000ULL, "15 m"},
{"SW 9", 24800000ULL, "12 m"},
{"SW10", 28000000ULL, "10 m"},
{"FM 1", 88000000ULL, "4 m"},
{"FM 2", 144000000ULL,"2 m"}
};
int currentBand = 4; // start from SW 1
const int NUM_BANDS = sizeof(bands)/sizeof(bands[0]);
// Compute band boundaries (end freq = next start - 1 Hz)
uint64_t bandEndFreq(int idx) {
if (idx >= NUM_BANDS - 1) return FREQ_MAX;
return bands[idx + 1].startFreq - 1ULL;
}
// --- UI / geometry ---
const int TOP_H=140, TOP_Y=0;
int16_t CX=120, CY=120;
TFT_eSprite spriteTop(&tft);
// radii
int16_t R_OUT_A=111, R_OUT_B=96; // outer dial
int16_t R_IN_A = 70, R_IN_B =62; // inner dial
// semicircle window (+ your -70° shift)
float ANG0=-120.0f, ANG1=+120.0f;
#define WINDOW_SHIFT_DEG (-70.0f)
// tick grid
const float TICK_MAJOR_STEP=30.0f; // majors each 30° (10 minors)
const float TICK_MINOR_STEP=3.0f; // minors each 3°
const int INNER_LABEL_EVERY_MINORS = 20; // label every 20 minors = 60°
// lengths & thickness
const int MINOR_LEN=4, MID_LEN=7, MAJOR_LEN=11;
const int THICK_MINOR=1, THICK_MID=2, THICK_MAJOR=3;
// mapping
#define OUTER_DEG_PER_HZ (0.003f) // 30° = 10 kHz → 3° = 1 kHz
#define INNER_RATIO (0.10f) // inner tape 10× slower (same dir)
// colors
#define COL_OUTER TFT_CYAN
#define COL_INNER TFT_GREEN
#define COL_CENTER TFT_RED
#define COL_LABEL TFT_WHITE
#define FRAME_COL TFT_DARKGREY
// frame
#define FRAME_R 118
#define FRAME_THICK 3
#define FRAME_POST_LEN 6
// encoder state
int8_t encQuart=0; uint32_t lastBtnMs=0;
volatile bool tweening=false;
// ---------- helpers ----------
static inline float d2r(float d){ return d*PI/180.0f; }
static inline float wrap360(float a){ a=fmodf(a,360.0f); if(a<0) a+=360.0f; return a; }
static inline bool inWindow(float ang,float V0,float V1){ float span=V1-V0; float rel=wrap360(ang-V0); return rel<=span; }
static inline int posmod(int a,int m){ int r=a%m; return (r<0)? r+m : r; }
String formatKHzEU_2dec(uint64_t hz){
uint64_t khz_hundredths=(hz+5ULL)/10ULL;
uint32_t frac2=(uint32_t)(khz_hundredths%100ULL);
uint64_t khz_int=khz_hundredths/100ULL;
char tmp[32]; snprintf(tmp,sizeof(tmp),"%llu",(unsigned long long)khz_int);
String sInt(tmp), sSep; int n=sInt.length();
for(int i=0;i<n;i++){ sSep+=sInt[i]; int left=n-i-1; if(left>0 && (left%3)==0) sSep+='.'; }
char buf[48]; snprintf(buf,sizeof(buf),"%s,%02u", sSep.c_str(), frac2);
return String(buf);
}
void drawTickThick(TFT_eSprite* s,float angDeg,int16_t rOuter,int16_t rInner,uint16_t col,int thickness){
float ang=d2r(angDeg); float nx=-sinf(ang), ny=cosf(ang);
for(int k=-(thickness/2); k<= (thickness/2); k++){
float offx=nx*k, offy=ny*k;
int16_t x1=CX + rOuter*cosf(ang) + offx;
int16_t y1=CY + rOuter*sinf(ang) + offy;
int16_t x2=CX + rInner*cosf(ang) + offx;
int16_t y2=CY + rInner*sinf(ang) + offy;
s->drawLine(x1,y1,x2,y2,col);
}
}
void drawTextAtAngle(TFT_eSprite* s,const String& txt,float ang,int16_t r,uint16_t col){
int16_t x=CX + r*cosf(d2r(ang));
int16_t y=CY + r*sinf(d2r(ang));
s->setTextDatum(MC_DATUM);
s->setTextColor(col,TFT_BLACK);
s->setTextFont(2);
s->drawString(txt,x,y);
}
void drawThickArcSprite(int16_t r,float V0,float V1,uint16_t col,int thickness){
for(int t=-(thickness/2); t<= (thickness/2); t++){
float step=2.0f;
for(float a=V0; a<=V1; a+=step){
int16_t x1=CX+(r+t)*cosf(d2r(a)), y1=CY+(r+t)*sinf(d2r(a));
float an=(a+step>V1)?V1:a+step;
int16_t x2=CX+(r+t)*cosf(d2r(an)), y2=CY+(r+t)*sinf(d2r(an));
spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL);
}
}
}
// ---------- TOP (sprite) ----------
void drawTopScalesSprite(uint64_t hz){
const float V0=ANG0+WINDOW_SHIFT_DEG, V1=ANG1+WINDOW_SHIFT_DEG;
// world “tapes” (angles)
const float tapeOut = (float)hz * OUTER_DEG_PER_HZ; // deg
const float tapeIn = (float)hz * OUTER_DEG_PER_HZ * INNER_RATIO;// deg
spriteTop.fillSprite(TFT_BLACK);
// ================= OUTER ticks =================
int nMin=(int)floorf((V0 - tapeOut)/TICK_MINOR_STEP) - 2;
int nMax=(int)ceilf ((V1 - tapeOut)/TICK_MINOR_STEP) + 2;
for(int n=nMin; n<=nMax; n++){
float ang=n*TICK_MINOR_STEP + tapeOut;
if(!inWindow(ang,V0,V1)) continue;
bool isMajor = (n % 10 == 0);
bool mid = (!isMajor) && (n % 5 == 0);
if(isMajor) continue; // majors drawn below
drawTickThick(&spriteTop, ang, R_OUT_A,
R_OUT_A - (mid?MID_LEN:MINOR_LEN),
COL_OUTER, (mid?THICK_MID:THICK_MINOR));
}
int mMin=(int)floorf((V0 - tapeOut)/TICK_MAJOR_STEP) - 1;
int mMax=(int)ceilf ((V1 - tapeOut)/TICK_MAJOR_STEP) + 1;
for(int m=mMin; m<=mMax; m++){
float ang = m*TICK_MAJOR_STEP + tapeOut;
if(!inWindow(ang,V0,V1)) continue;
drawTickThick(&spriteTop, ang, R_OUT_A, R_OUT_A - MAJOR_LEN, COL_OUTER, THICK_MAJOR);
// glued label; reversed; −30
int tsel = (UPPER_DIR > 0) ? m : -m;
int tens = posmod(tsel,10);
tens = posmod(tens - 3, 10);
char up[4]; snprintf(up, sizeof(up), "%d0", tens);
drawTextAtAngle(&spriteTop, up, ang, R_OUT_B - 12, COL_LABEL);
}
// ================= INNER ticks =================
int niMin=(int)floorf((V0 - tapeIn)/TICK_MINOR_STEP) - 2;
int niMax=(int)ceilf ((V1 - tapeIn)/TICK_MINOR_STEP) + 2;
for(int n=niMin; n<=niMax; n++){
float ang=n*TICK_MINOR_STEP + tapeIn;
if(!inWindow(ang,V0,V1)) continue;
bool isMajor = (n % 10 == 0);
bool mid = (!isMajor) && (n % 5 == 0);
drawTickThick(&spriteTop, ang, R_IN_A,
R_IN_A - (isMajor?MAJOR_LEN:(mid?MID_LEN:MINOR_LEN)),
COL_INNER, (isMajor?THICK_MAJOR:(mid?THICK_MID:THICK_MINOR)));
}
// --------- NEW: INNER labels every 20 minors (60°), matching actual frequency ---------
// For angles ai = k*60° + tapeIn (k integer), compute frequency at that angle:
// f_at = hz + (ai - aCenter) / (OUTER_DEG_PER_HZ*INNER_RATIO)
const float STEP_60 = TICK_MINOR_STEP * INNER_LABEL_EVERY_MINORS; // 60°
const float aCenter = (V0 + V1) * 0.5f;
int kMin = (int)floorf((V0 - tapeIn)/STEP_60) - 1;
int kMax = (int)ceilf ((V1 - tapeIn)/STEP_60) + 1;
for(int k=kMin; k<=kMax; k++){
float ai = k*STEP_60 + tapeIn;
if(!inWindow(ai, V0, V1)) continue;
// frequency at this angular position (round to 0.1 MHz)
float deltaDeg = ai - aCenter;
float deltaHz = -(deltaDeg) / (OUTER_DEG_PER_HZ * INNER_RATIO);
int64_t f_at = (int64_t)hz + (int64_t)lroundf(deltaHz) - 100000LL;
if (f_at < 0) f_at = 0;
if (f_at > 160000000LL) f_at = 160000000LL;
int64_t tenthsMHz = (f_at + 50000LL) / 100000LL;
char lo[14];
snprintf(lo, sizeof(lo), "%lld.%01lld",
(long long)(tenthsMHz / 10),
(long long)(tenthsMHz % 10));
drawTextAtAngle(&spriteTop, String(lo), ai, R_IN_B - 15, COL_LABEL);
}
// ================= Frame + red line =================
// arc
for (int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++){
float step=2.0f;
for(float a=V0; a<=V1; a+=step){
int16_t x1=CX+(FRAME_R+t)*cosf(d2r(a)), y1=CY+(FRAME_R+t)*sinf(d2r(a));
float an=(a+step>V1)?V1:a+step;
int16_t x2=CX+(FRAME_R+t)*cosf(d2r(an)), y2=CY+(FRAME_R+t)*sinf(d2r(an));
spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL);
}
}
const int16_t xL=CX+FRAME_R*cosf(d2r(V0)), yL=CY+FRAME_R*sinf(d2r(V0));
const int16_t xR=CX+FRAME_R*cosf(d2r(V1)), yR=CY+FRAME_R*sinf(d2r(V1));
int16_t HLINE_Y = (((yL+yR)/2) - 1); if(HLINE_Y>(TOP_H-2)) HLINE_Y=TOP_H-2;
for(int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++) spriteTop.drawLine(5, HLINE_Y+t, 235, HLINE_Y+t, FRAME_COL);
spriteTop.drawLine(xL, HLINE_Y, xL, HLINE_Y - FRAME_POST_LEN, FRAME_COL);
spriteTop.drawLine(xR, HLINE_Y, xR, HLINE_Y - FRAME_POST_LEN, FRAME_COL);
// red center line
const int16_t RED_TOP_Y=CY-105, RED_BOT_Y=HLINE_Y;
for(int t=-1; t<=1; t++) spriteTop.drawLine(CX+t, RED_BOT_Y, CX+t, RED_TOP_Y, COL_CENTER);
spriteTop.pushSprite(0, TOP_Y);
}
// ---------- bottom readout ----------
String formatKHzEU_2dec(uint64_t); // already defined above
void drawFreqBox(uint64_t hz, uint32_t step) {
// --- smaller frame, moved up ---
int bx = 25; // reduced left margin (was 25)
int by = 162; // 5 px below STEP label (was 180)
int bw = 190; // narrower box (was 190)
int bh = 36;
int br = 6;
// frame
tft.drawRoundRect(bx, by, bw, bh, br, TFT_WHITE);
tft.fillRoundRect(bx + 2, by + 2, bw - 4, bh - 4, br, TFT_BLACK);
// --- frequency number ---
tft.setTextDatum(ML_DATUM);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.setTextFont(4);
String freqStr;
String unitStr;
char buf[32];
if (hz < 1000000ULL) {
// Below 1 MHz → show in kHz, two decimals
double kHz = hz / 1000.0;
snprintf(buf, sizeof(buf), "%.2f", kHz);
freqStr = String(buf);
unitStr = "KHz";
} else if (hz < 100000000ULL) {
// 1–99.999 MHz → show full kHz precision (like 11.880,00 MHz)
double MHz = hz / 1000000.0;
uint32_t whole = (uint32_t)MHz;
uint32_t frac = (uint32_t)((MHz - whole) * 1000000.0 + 0.5); // Hz remainder
// Format as ###.###,## (European style)
snprintf(buf, sizeof(buf), "%lu.%03lu,%02lu",
(unsigned long)whole,
(unsigned long)(frac / 1000),
(unsigned long)((frac / 10) % 100));
freqStr = String(buf);
unitStr = "MHz";
} else {
// ≥100 MHz → show simplified "M" unit
double MHz = hz / 1000000.0;
snprintf(buf, sizeof(buf), "%.2f", MHz);
freqStr = String(buf);
unitStr = "MHz";
}
int textY = by + 2 + bh / 2;
// frequency number
tft.drawString(freqStr, bx + 8, textY);
// --- unit label ---
tft.setTextFont(4);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.setTextDatum(ML_DATUM);
// place close to number (shift left ~15 px)
tft.drawString(unitStr, bx + bw - 60, textY);
// --- STEP label under gray frame line ---
int stepY = 150; // adjust: ~5 px below the gray horizontal line
int stepX = 120; // centered
// clear old area
tft.fillRect(60, stepY - 10, 120, 22, TFT_BLACK);
// decide text
char stepStr[12];
if (step == 10) strcpy(stepStr, "10 Hz");
else if (step == 100) strcpy(stepStr, "100 Hz");
else if (step == 1000) strcpy(stepStr, "1 KHz");
else if (step == 10000) strcpy(stepStr, "10 KHz");
else if (step == 100000) strcpy(stepStr, "100 KHz");
else if (step == 1000000) strcpy(stepStr, "1 MHz");
else snprintf(stepStr, sizeof(stepStr), "%lu Hz", (unsigned long)step);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(2);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString(String("STEP: ") + stepStr, stepX, stepY);
}
void drawBottomFrameOnce(){ drawFreqBox(vfoHz, stepLadder[stepIndex]); }
// ---------- encoder ----------
int8_t readEncoderTransition(){ static int last=0; int a=digitalRead(ENC_A), b=digitalRead(ENC_B); int val=(a<<1)|b;
static const int8_t trans[16]={0,-1,+1,0,+1,0,0,-1,-1,0,0,+1,0,+1,-1,0};
int8_t d=trans[(last<<2)|val]; last=val; return d; }
int8_t readEncoderDetent(){ if(tweening) return 0;
int8_t t=readEncoderTransition(); if(t){ encQuart+=t; if(encQuart>=4){encQuart=0; return +1;} if(encQuart<=-4){encQuart=0; return -1;} } return 0; }
// ---------- tween (unchanged from your good version) ----------
static inline uint32_t tweenSubstep(uint32_t stepHz){
if (stepHz >= 1000000)return 100000; // 1 MHz → 10×100 kHz
if (stepHz >= 100000) return 10000; // 100 kHz → 10×10 kHz
if (stepHz >= 10000) return 1000; // 10 kHz → 10×1 kHz
return stepHz;
}
void updateBandFromFreq() {
// check which band the current frequency belongs to
for (int i = 0; i < NUM_BANDS; i++) {
uint64_t start = bands[i].startFreq;
uint64_t end = bandEndFreq(i);
if (vfoHz >= start && vfoHz <= end) {
if (currentBand != i) {
bool up = (i > currentBand); // remember direction
currentBand = i;
drawBandInfo(stepLadder[stepIndex]); // redraw SW x / STEP / m info
flashButton(up ? 1 : 0); // flash B+ if up, B– if down
}
break;
}
}
}
void applyTuningAndRender(int8_t clicks) {
if (clicks == 0) return;
uint32_t stepHz = stepLadder[stepIndex];
int64_t delta = (int64_t)clicks * (int64_t)stepHz;
int64_t next = (int64_t)vfoHz + delta;
if (next < (int64_t)FREQ_MIN) next = FREQ_MIN;
if (next > (int64_t)FREQ_MAX) next = FREQ_MAX;
uint64_t from = vfoHz;
uint64_t to = (uint64_t)next;
uint32_t sub = tweenSubstep(stepHz);
if (sub < stepHz) {
tweening = true;
int dir = (to > from) ? +1 : -1;
uint64_t cur = from;
while (cur != to) {
uint64_t nextStep = (dir > 0) ? (cur + sub) : (cur >= sub ? cur - sub : 0);
if ((dir > 0 && nextStep > to) || (dir < 0 && nextStep < to)) nextStep = to;
drawTopScalesSprite(nextStep);
cur = nextStep;
yield();
}
vfoHz = to;
} else {
vfoHz = to;
}
si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
drawTopScalesSprite(vfoHz);
drawFreqBox(vfoHz, stepHz);
updateBandFromFreq(); // <-- new call here
tweening = false;
}
// ---------- Si5351 ----------
bool siInit(){ Wire.begin(SI5351_SDA, SI5351_SCL, 400000);
if(!si5351.init(SI5351_CRYSTAL_LOAD_8PF,0,SI5351_CORR_PPM)) return false;
si5351.output_enable(SI5351_CLK0,1); si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); return true; }
void drawBandInfo(uint32_t step) {
int y = 150; // same baseline as STEP label
tft.fillRect(10, y - 10, 220, 22, TFT_BLACK);
tft.setTextFont(2);
tft.setTextDatum(MC_DATUM);
// Left part – band name (red)
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString(bands[currentBand].name, 35, y);
// Middle part – STEP label (yellow)
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
char stepStr[12];
if (step == 10) strcpy(stepStr, "10 Hz");
else if (step == 100) strcpy(stepStr, "100 Hz");
else if (step == 1000) strcpy(stepStr, "1 KHz");
else if (step == 10000) strcpy(stepStr, "10 KHz");
else if (step == 100000) strcpy(stepStr, "100 KHz");
else if (step == 1000000) strcpy(stepStr, "1 MHz");
else sprintf(stepStr, "%lu Hz", (unsigned long)step);
tft.drawString(String("STEP: ") + stepStr, 120, y);
// Right part – wavelength (red)
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString(bands[currentBand].wavelength, 200, y);
}
// ---------- setup / loop ----------
void setup(){
Serial.begin(115200); delay(100);
ring.begin();
ring.setBrightness(LED_BRIGHTNESS); // soft brightness
ring.clear();
ring.show();
panelPowerOn(); backlightInit(0); pulseResetPin();
tft.init(); tft.setRotation(0);tft.setRotation(0);
// --- Turn on backlight gradually ---
for (int d = 0; d <= 200; d += 10) {
ledcWrite(BL_CHANNEL, d);
delay(10);
}
// --- Splash / Intro Screen (before UI setup) ---
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("Retro Style", 120, 90);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.drawString("VFO", 120, 120);
tft.setTextFont(2);
tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
tft.drawString("by mircemk", 120, 150);
delay(2000);
tft.fillScreen(TFT_BLACK);
// --- Now create sprites and draw the main UI ---
spriteTop.setColorDepth(16);
spriteTop.createSprite(240, TOP_H);
spriteTop.setTextDatum(MC_DATUM);
pinMode(ENC_A, INPUT_PULLUP);
pinMode(ENC_B, INPUT_PULLUP);
pinMode(ENC_BTN, INPUT_PULLUP);
tft.fillScreen(TFT_BLACK);
drawTopScalesSprite(vfoHz);
drawBottomFrameOnce();
drawTouchButtons();
if (siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
// --- Initialize and show the correct band immediately ---
updateBandFromFreq();
drawBandInfo(stepLadder[stepIndex]);
if(siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
// --- Initialize band display based on starting frequency ---
updateBandFromFreq(); // detect which band 10.100 MHz belongs to
drawBandInfo(stepLadder[stepIndex]); // draw SW5 30 m info immediately
// --- Enable main panel power (required for touch rail) ---
pinMode(1, OUTPUT); digitalWrite(1, HIGH);
pinMode(2, OUTPUT); digitalWrite(2, HIGH);
delay(20);
// --- Reset and start the touch controller ---
pinMode(TP_RST, OUTPUT);
digitalWrite(TP_RST, LOW);
delay(10);
digitalWrite(TP_RST, HIGH);
delay(50);
Wire.begin(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN);
touch.begin();
Serial.println("Touch initialized (CrowPanel 1.28)");
}
void drawTouchButtons() {
// Button geometry
int btnW = 80, btnH = 36;
int btnY = 205; // bottom area
int btnLeftX = 35; // left button
int btnRightX = 123; // right button
uint16_t btnColor = tft.color565(255, 140, 0); // orange
// --- LEFT (-1) ---
tft.fillRoundRect(btnLeftX, btnY, btnW, btnH, 6, btnColor);
tft.drawRoundRect(btnLeftX, btnY, btnW, btnH, 6, TFT_WHITE);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
tft.setTextColor(TFT_WHITE, btnColor);
tft.drawString("-B", btnLeftX + btnW/2 +10, btnY + btnH/2);
// --- RIGHT (+1) ---
tft.fillRoundRect(btnRightX, btnY, btnW, btnH, 6, btnColor);
tft.drawRoundRect(btnRightX, btnY, btnW, btnH, 6, TFT_WHITE);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
tft.setTextColor(TFT_WHITE, btnColor);
tft.drawString("+B", btnRightX + btnW/2 - 10, btnY + btnH/2);
}
void flashButton(int btn) {
// btn: 0 = B– , 1 = B+
int btnX = (btn == 0) ? 35 : 123;
int btnY = 205;
int btnW = 80, btnH = 36;
// Flash red for 80 ms then return to orange
uint16_t red = tft.color565(255, 0, 0);
uint16_t orange = tft.color565(255, 140, 0);
// show red
tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, red);
tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
tft.setTextColor(TFT_WHITE, red);
tft.drawString((btn == 0) ? "-B" : "+B",
btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2);
delay(FLASH_TIME_MS);
// back to orange
tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, orange);
tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE);
tft.setTextColor(TFT_WHITE, orange);
tft.drawString((btn == 0) ? "-B" : "+B",
btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2);
}
// direction >0 → clockwise (right turn), direction <0 → counterclockwise
void updateLedRing(int direction) {
// reverse rotation logic so it matches encoder
if (direction > 0) ledPos = (ledPos - 1 + LED_COUNT) % LED_COUNT;
else if (direction < 0) ledPos = (ledPos + 1) % LED_COUNT;
// draw single glowing yellow LED
ring.clear();
ring.setPixelColor(ledPos, ring.Color(255, 180, 0)); // warm yellow
ring.show();
}
void loop() {
int8_t det = readEncoderDetent();
if (det) applyTuningAndRender(det);
updateLedRing(det);
uint32_t now = millis();
bool pressed = (digitalRead(ENC_BTN) == LOW);
if (!tweening && pressed && (now - lastBtnMs) > 250) {
lastBtnMs = now;
stepIndex = (stepIndex + 1) % (sizeof(stepLadder) / sizeof(stepLadder[0]));
drawFreqBox(vfoHz, stepLadder[stepIndex]);
}
// --- Touch reading block (runs always) ---
uint16_t x, y;
uint8_t gesture;
static uint32_t lastTouchMs = 0;
if (millis() - lastTouchMs > 30) { // poll every 30 ms
lastTouchMs = millis();
bool touched = touch.getTouch(&x, &y, &gesture);
if (touched) {
Serial.printf("Touch: X=%u Y=%u Gesture=0x%02X\n", x, y, gesture);
if (y > 205 && y < 245) { // bottom strip only
if (x >= 25 && x <= 115) { // left button
flashButton(0); // fade red→orange
if (currentBand > 0) currentBand--;
}
else if (x >= 125 && x <= 215) { // right button
flashButton(1);
if (currentBand < NUM_BANDS - 1) currentBand++;
}
vfoHz = bands[currentBand].startFreq;
si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
drawTopScalesSprite(vfoHz);
drawFreqBox(vfoHz, stepLadder[stepIndex]);
drawBandInfo(stepLadder[stepIndex]);
}
}
}
delay(5); // watchdog-friendly pause
}
Comments