/* KE0WPC dedicated 2 m CW beacon firmware for the Quansheng UV-K5 (DP32G030)
 *
 * This replaces the whole radio application. There is no receiver, no VFO,
 * no menu and no keypad entry. The radio powers up, waits out an arming
 * delay, then transmits a CW identification followed by a steady carrier,
 * forever, on one hard-coded frequency.
 *
 * Built on the driver layer from DualTachyon / egzumer / F4HWN.
 * Apache License, Version 2.0.
 *
 * ---------------------------------------------------------------------
 * HOW THE CW IS GENERATED
 *
 * The BK4819 has no CW mode. What we do instead is bring the transmitter
 * up in FM mode with the microphone path muted, which yields a clean
 * unmodulated carrier, and then key that carrier on and off by ramping the
 * PA bias register (REG_36) up and down. That is true A1A, a distant
 * station copies it on a CW or SSB receiver as a tone at whatever offset
 * they are tuned to.
 *
 * The ramp is there on purpose. Slamming the bias from 0 to full in one
 * register write produces a hard-edged envelope and audible key clicks
 * spread either side of the carrier. Sixteen steps over about four
 * milliseconds rounds the edges off enough to be polite.
 *
 * ---------------------------------------------------------------------
 * DUTY CYCLE - READ THIS
 *
 * A UV-K5 is a handheld. Its PA is rated for the intermittent duty an
 * operator produces, not for beacon service. With the stock defaults here
 * (about 15 s of transmit in every 60 s, at low power) it runs warm but
 * survives. If you raise the power, lengthen the carrier or shorten the
 * gap, you are on your own thermally. Bolt a heatsink to the back of the
 * case, feed it from a bench supply rather than the battery, and put a
 * thermometer on it for the first few hours. Overall, I have found that
 * a 30 second wait interval keeps the a radio at a reasonable temperature.
 */

#include <stdbool.h>
#include <stdint.h>
#include <string.h>

#include "beacon_config.h"
#include "morse.h"

#include "bsp/dp32g030/gpio.h"
#include "bsp/dp32g030/syscon.h"

#include "board.h"
#include "driver/backlight.h"
#include "driver/bk4819.h"
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "driver/keyboard.h"
#include "driver/st7565.h"
#include "driver/system.h"
#include "driver/systick.h"
#include "font.h"

#define BEACON_FREQ  ((uint32_t)(BEACON_FREQ_10HZ + (BEACON_FREQ_TRIM_10HZ)))

/* 2 m lands in BAND3_137MHz, index 2, in the stock band table. */
#define BEACON_BAND_INDEX   2u
#define BAND3_LOWER         13700000u
#define BAND3_UPPER         17400000u

/* ------------------------------------------------------------------ */
/* State                                                               */
/* ------------------------------------------------------------------ */

static uint8_t  gPaBias;        /* calibrated PA bias for our frequency */
static bool     gAborted;       /* key pressed -> stop for good         */
static uint32_t gCycles;        /* completed transmissions              */
static uint16_t gBattCal[6];    /* battery calibration from EEPROM      */

volatile uint32_t gTicks10ms;

/* SysTick fires every 10 ms. Keep this trivial - it runs during keying. */
void SystickHandler(void);
void SystickHandler(void)
{
    gTicks10ms++;
}

/* printf is not linked, but the driver layer declares a putchar hook. */
void _putchar(char c);
void _putchar(__attribute__((unused)) char c) { }

/* ------------------------------------------------------------------ */
/* Tiny display helpers                                                */
/* ------------------------------------------------------------------ */

static void ScreenClear(void)
{
    memset(gFrameBuffer, 0, sizeof(gFrameBuffer));
    memset(gStatusLine, 0, sizeof(gStatusLine));
}

/* Same 6 px font and 7 px pitch the stock UI uses. */
static void ScreenText(uint8_t line, uint8_t col, const char *s)
{
    if (line >= FRAME_LINES)
        return;

    uint8_t *buf = gFrameBuffer[line];

    for (uint8_t i = 0; s[i] != '\0'; i++) {
        const unsigned int x = (col + i) * 7u + 1u;
        if (x + 6u > LCD_WIDTH)
            break;
        if (s[i] > ' ' && s[i] < 127) {
            const unsigned int idx = (unsigned int)(s[i] - ' ' - 1);
            memcpy(buf + x, &gFontSmall[idx][0], 6);
        }
    }
}

static void ScreenShow(void)
{
    ST7565_BlitStatusLine();
    ST7565_BlitFullScreen();
}

/* Unsigned integer to string, no printf. */
static void UIntToStr(uint32_t v, char *out, uint8_t min_digits)
{
    char tmp[11];
    uint8_t n = 0;

    do {
        tmp[n++] = (char)('0' + (v % 10u));
        v /= 10u;
    } while (v != 0u && n < sizeof(tmp));

    while (n < min_digits && n < sizeof(tmp))
        tmp[n++] = '0';

    uint8_t k = 0;
    while (n > 0)
        out[k++] = tmp[--n];
    out[k] = '\0';
}

/* ------------------------------------------------------------------ */
/* Abort handling                                                      */
/* ------------------------------------------------------------------ */

static bool CheckAbort(void)
{
#if BEACON_KEY_ABORT
    if (!gAborted && KEYBOARD_Poll() != KEY_INVALID)
        gAborted = true;
#endif
    return gAborted;
}

/* Delay that stays responsive to the abort key and does not overflow the
 * microsecond delay helper on long waits. */
static void DelayMsAbortable(uint32_t ms)
{
    while (ms > 0u && !gAborted) {
        const uint32_t chunk = (ms > 10u) ? 10u : ms;
        SYSTEM_DelayMs(chunk);
        ms -= chunk;
        CheckAbort();
    }
}

/* Non-abortable, used inside the keying envelope where we must not leave
 * the PA in a half-biased state. */
static void DelayUsRaw(uint32_t us)
{
    SYSTICK_DelayUs(us);
}

/* ------------------------------------------------------------------ */
/* Calibration                                                         */
/* ------------------------------------------------------------------ */

/* Linear interpolation between the three factory calibration points for
 * this band, exactly as the stock firmware does it. */
static uint8_t InterpolateBias(uint8_t lo, uint8_t mid, uint8_t hi,
                               uint32_t f_lower, uint32_t f_mid,
                               uint32_t f_upper, uint32_t f)
{
    if (f <= f_lower) return lo;
    if (f >= f_upper) return hi;

    if (f <= f_mid) {
        int32_t v = mid + (((int32_t)mid - (int32_t)lo) *
                           (int32_t)(f - f_lower)) / (int32_t)(f_mid - f_lower);
        return (uint8_t)((v < 0) ? 0 : (v > 255 ? 255 : v));
    }

    int32_t v = mid + (((int32_t)hi - (int32_t)mid) *
                       (int32_t)(f - f_mid)) / (int32_t)(f_upper - f_mid);
    return (uint8_t)((v < 0) ? 0 : (v > 255 ? 255 : v));
}

static void LoadCalibration(void)
{
    uint8_t txp[3];

    /* Factory TX power calibration table: 0x1ED0, 16 bytes per band,
     * 3 bytes per power level (low / mid / high). */
    EEPROM_ReadBuffer(0x1ED0u + (BEACON_BAND_INDEX * 16u) +
                      ((uint32_t)BEACON_POWER_LEVEL * 3u), txp, 3);

    const uint32_t mid = (BAND3_LOWER + BAND3_UPPER) / 2u;

    gPaBias = InterpolateBias(txp[0], txp[1], txp[2],
                              BAND3_LOWER, mid, BAND3_UPPER, BEACON_FREQ);

    /* A blank or corrupt EEPROM would otherwise give us a wild bias. */
#if BEACON_BIAS_CLAMP < 255
    if (gPaBias > BEACON_BIAS_CLAMP)
        gPaBias = BEACON_BIAS_CLAMP;
#endif

    EEPROM_ReadBuffer(0x1F40u, gBattCal, 12);
    if (gBattCal[3] == 0u || gBattCal[3] >= 5000u)
        gBattCal[3] = 2000u;   /* fall back to a sane nominal */
}

/* Returns battery voltage in units of 10 mV. */
static uint16_t BatteryVoltage(void)
{
    uint16_t raw, current;
    BOARD_ADC_GetBatteryInfo(&raw, &current);
    return (uint16_t)(((uint32_t)raw * 760u) / gBattCal[3]);
}

/* ------------------------------------------------------------------ */
/* Transmitter                                                         */
/* ------------------------------------------------------------------ */

static void TxSetup(void)
{
    /* Kill the audio path so nothing can modulate the carrier. */
    GPIO_ClearBit(&GPIOC->DATA, GPIOC_PIN_AUDIO_PATH);

    BK4819_ToggleGpioOut(BK4819_GPIO0_PIN28_RX_ENABLE, false);

    BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, false);
    BK4819_SetFrequency(BEACON_FREQ);
    BK4819_SetCompander(0);

    BK4819_PrepareTransmit();
    SYSTEM_DelayMs(10);

    BK4819_PickRXFilterPathBasedOnFrequency(BEACON_FREQ);

    /* No CTCSS, no DCS, no mic audio. Carrier only. */
    BK4819_ExitSubAu();
    BK4819_EnterTxMute();

    BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, true);
    SYSTEM_DelayMs(5);

    /* Start with the PA fully off - keying brings it up. */
    BK4819_SetupPowerAmplifier(0, BEACON_FREQ);
    SYSTEM_DelayMs(5);
}

static void TxShutdown(void)
{
    BK4819_SetupPowerAmplifier(0, BEACON_FREQ);
    BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
    BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false);
    BK4819_Idle();
}

static void KeyDown(void)
{
    BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, true);

#if BEACON_HARD_KEY
    BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, true);
#endif

    for (uint8_t i = 1; i <= BEACON_RAMP_STEPS; i++) {
        const uint8_t bias =
            (uint8_t)(((uint32_t)gPaBias * i) / BEACON_RAMP_STEPS);
        BK4819_SetupPowerAmplifier(bias, BEACON_FREQ);
        DelayUsRaw(BEACON_RAMP_STEP_US);
    }
    BK4819_SetupPowerAmplifier(gPaBias, BEACON_FREQ);
}

static void KeyUp(void)
{
    for (uint8_t i = BEACON_RAMP_STEPS; i > 0; i--) {
        const uint8_t bias =
            (uint8_t)(((uint32_t)gPaBias * (i - 1)) / BEACON_RAMP_STEPS);
        BK4819_SetupPowerAmplifier(bias, BEACON_FREQ);
        DelayUsRaw(BEACON_RAMP_STEP_US);
    }
    BK4819_SetupPowerAmplifier(0, BEACON_FREQ);

#if BEACON_HARD_KEY
    BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
#endif

    BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false);
}

static const morse_io_t gMorseIo = {
    .key_down = KeyDown,
    .key_up   = KeyUp,
    .delay_ms = DelayMsAbortable,
    .aborted  = CheckAbort,

    /* Rise plus fall, rounded up, in milliseconds. */
    .key_overhead_ms = (uint16_t)
        (((2u * BEACON_RAMP_STEPS * BEACON_RAMP_STEP_US) + 999u) / 1000u),
};

/* ------------------------------------------------------------------ */
/* Screens                                                             */
/* ------------------------------------------------------------------ */

static void ShowStatus(const char *state, uint32_t counter)
{
    char num[12];

    ScreenClear();
    ScreenText(0, 0, BEACON_CALLSIGN " BEACON");
    ScreenText(1, 0, "144.2770 MHz CW");
    ScreenText(2, 0, state);

    if (counter != 0xFFFFFFFFu) {
        UIntToStr(counter, num, 1);
        ScreenText(2, 12, num);
    }

    UIntToStr(gCycles, num, 1);
    ScreenText(4, 0, "TX cycles:");
    ScreenText(4, 11, num);

    const uint16_t v = BatteryVoltage();
    UIntToStr(v / 100u, num, 1);
    ScreenText(5, 0, "Batt:");
    ScreenText(5, 6, num);
    ScreenText(5, 7, ".");
    UIntToStr(v % 100u, num, 2);
    ScreenText(5, 8, num);
    ScreenText(5, 10, "V");

    ScreenShow();
}

static void ShowHalted(const char *why)
{
    ScreenClear();
    ScreenText(0, 0, BEACON_CALLSIGN " BEACON");
    ScreenText(2, 0, "*** HALTED ***");
    ScreenText(3, 0, why);
    ScreenText(5, 0, "Power cycle to");
    ScreenText(6, 0, "restart.");
    ScreenShow();
}

/* ------------------------------------------------------------------ */
/* Entry point                                                         */
/* ------------------------------------------------------------------ */

void Main(void)
{
    SYSCON_DEV_CLK_GATE = 0
        | SYSCON_DEV_CLK_GATE_GPIOA_BITS_ENABLE
        | SYSCON_DEV_CLK_GATE_GPIOB_BITS_ENABLE
        | SYSCON_DEV_CLK_GATE_GPIOC_BITS_ENABLE
        | SYSCON_DEV_CLK_GATE_SPI0_BITS_ENABLE
        | SYSCON_DEV_CLK_GATE_SARADC_BITS_ENABLE
        | SYSCON_DEV_CLK_GATE_PWM_PLUS0_BITS_ENABLE;

    SYSTICK_Init();
    BOARD_Init();
    BK4819_Init();

    BACKLIGHT_SetBrightness(4);

    LoadCalibration();

    /* --- arming delay: last chance to notice a missing antenna --- */
    for (uint32_t s = BEACON_ARM_DELAY_SEC; s > 0u; s--) {
        ShowStatus("ARMING", s);
        DelayMsAbortable(1000);
        if (gAborted) {
            ShowHalted("Key pressed");
            for (;;) { }
        }
    }

    BACKLIGHT_SetBrightness(0);   /* nobody watches an unattended beacon */

    /* --- main cycle --- */
    for (;;) {

#if BEACON_LOW_BATT_10MV > 0
        if (BatteryVoltage() < BEACON_LOW_BATT_10MV) {
            TxShutdown();
            ShowHalted("Battery low");
            for (;;) { }
        }
#endif

        ShowStatus("ID", 0xFFFFFFFFu);
        TxSetup();

        const bool sent = MORSE_SendString(&gMorseIo, BEACON_ID_TEXT,
                                           BEACON_WPM);

        if (sent) {
#if BEACON_CARRIER_SEC > 0
            ShowStatus("CARRIER", BEACON_CARRIER_SEC);
            KeyDown();
            DelayMsAbortable((uint32_t)BEACON_CARRIER_SEC * 1000u);
            KeyUp();
#endif
            gCycles++;
        }

        TxShutdown();

        if (gAborted) {
            ShowHalted("Key pressed");
            for (;;) { }
        }

        for (uint32_t s = BEACON_GAP_SEC; s > 0u; s--) {
            ShowStatus("WAIT", s);
            DelayMsAbortable(1000);
            if (gAborted) {
                ShowHalted("Key pressed");
                for (;;) { }
            }
        }
    }
}
