PocketLink UHF — מכשיר קשר חכם עם חוגה

Code for reading and copying

בסוף הצעד: הקוד עולה והמסך אומר RADIO OK

This code is read-only, so it cannot be changed by accident.

src/main.cpp cpp
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// PocketLink UHF — XIAO ESP32-C3 + SA868S-UHF
// ברירת המחדל היא RX בלבד. שינוי TX_ARMED מותר רק לבעל רישיון,
// על תדר שהוקצה לו, ותחילה לעומס דמה 50Ω.
constexpr bool TX_ARMED = false;

constexpr uint8_t PIN_RADIO_PTT = D0;  // אל SA868S pin 5, עם pull-up של 10k
constexpr uint8_t PIN_ENC_A = D1;
constexpr uint8_t PIN_ENC_B = D2;
constexpr uint8_t PIN_ENC_SW = D3;
constexpr uint8_t PIN_USER_PTT = D10;
constexpr uint8_t PIN_I2C_SDA = D4;
constexpr uint8_t PIN_I2C_SCL = D5;
constexpr uint8_t PIN_RADIO_TX = D6;   // אל SA868S RXD, pin 16
constexpr uint8_t PIN_RADIO_RX = D7;   // אל SA868S TXD, pin 17

constexpr uint8_t OLED_ADDR = 0x3C;
constexpr uint8_t OLED_W = 128;
constexpr uint8_t OLED_H = 64;
Adafruit_SSD1306 display(OLED_W, OLED_H, &Wire, -1);
HardwareSerial radio(1);

struct Channel {
  const char *name;
  const char *rxMHz;
  const char *txMHz;
  const char *ctcss;
  bool txAllowed;
};

// דוגמאות קליטה בפס 70 ס"מ. לפני שימוש אמיתי מחליפים בהתאם לרישיון
// ולתכנית הפס העדכנית. שום ערוץ אינו מורשה לשידור בקובץ שמגיע מהאתר.
Channel channels[] = {
    {"70CM 1", "430.0000", "430.0000", "0000", false},
    {"70CM 2", "433.5000", "433.5000", "0000", false},
    {"70CM 3", "438.5000", "438.5000", "0000", false},
    {"70CM 4", "439.0000", "439.0000", "0000", false},
};
constexpr size_t CHANNEL_COUNT = sizeof(channels) / sizeof(channels[0]);

size_t channelIndex = 0;
int lastEncA = HIGH;
bool transmitting = false;
bool moduleReady = false;
String lastReply;
unsigned long lastRssiAt = 0;
unsigned long lastDrawAt = 0;
int rssiValue = -1;

String readReply(uint32_t timeoutMs = 350) {
  String reply;
  const uint32_t started = millis();
  while (millis() - started < timeoutMs) {
    while (radio.available()) {
      const char c = static_cast<char>(radio.read());
      reply += c;
      if (reply.endsWith("\r\n")) return reply;
    }
    delay(1);
  }
  return reply;
}

bool command(const String &text, const char *successToken, uint32_t timeoutMs = 350) {
  while (radio.available()) radio.read();
  radio.print(text);
  radio.print("\r\n");
  lastReply = readReply(timeoutMs);
  return lastReply.indexOf(successToken) >= 0;
}

bool configureChannel() {
  const Channel &ch = channels[channelIndex];
  // 1 = low power. גם אם TX ייפתח בעתיד, מתחילים בהספק הנמוך.
  const String group = "AT+DMOSETGROUP=1," + String(ch.txMHz) + "," +
                       String(ch.rxMHz) + "," + String(ch.ctcss) + ",4," +
                       String(ch.ctcss);
  return command(group, "+DMOSETGROUP:0", 600);
}

void drawScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("POCKETLINK  ");
  display.println(moduleReady ? "RADIO OK" : "CHECK RADIO");

  display.setTextSize(2);
  display.setCursor(0, 16);
  display.println(channels[channelIndex].rxMHz);

  display.setTextSize(1);
  display.setCursor(0, 42);
  display.print(channelIndex + 1);
  display.print("/");
  display.print(CHANNEL_COUNT);
  display.print(" ");
  display.print(channels[channelIndex].name);

  display.setCursor(0, 54);
  if (transmitting) {
    display.print("TX");
  } else if (!TX_ARMED) {
    display.print("RX ONLY");
  } else {
    display.print("RX READY");
  }
  if (rssiValue >= 0) {
    display.print("  RSSI ");
    display.print(rssiValue);
  }
  display.display();
}

void setChannel(int direction) {
  if (transmitting) return;
  const int next =
      (static_cast<int>(channelIndex) + direction + CHANNEL_COUNT) % CHANNEL_COUNT;
  if (next == static_cast<int>(channelIndex)) return;
  channelIndex = static_cast<size_t>(next);
  moduleReady = configureChannel();
  rssiValue = -1;
  drawScreen();
}

void pollEncoder() {
  const int a = digitalRead(PIN_ENC_A);
  if (a != lastEncA && a == LOW) {
    const int direction = digitalRead(PIN_ENC_B) == HIGH ? 1 : -1;
    setChannel(direction);
  }
  lastEncA = a;
}

void pollPtt() {
  const bool pressed = digitalRead(PIN_USER_PTT) == LOW;
  const bool mayTransmit = TX_ARMED && channels[channelIndex].txAllowed;
  const bool nextTx = pressed && mayTransmit;
  if (nextTx == transmitting) return;

  transmitting = nextTx;
  digitalWrite(PIN_RADIO_PTT, transmitting ? LOW : HIGH);
  drawScreen();
}

void requestRssi() {
  if (transmitting || millis() - lastRssiAt < 1500) return;
  lastRssiAt = millis();
  if (!command("AT+RSSI?", "RSSI:", 250)) return;
  const int colon = lastReply.indexOf(':');
  if (colon >= 0) rssiValue = lastReply.substring(colon + 1).toInt();
}

void setup() {
  pinMode(PIN_RADIO_PTT, OUTPUT);
  digitalWrite(PIN_RADIO_PTT, HIGH);  // RX לפני כל אתחול של התצוגה/רדיו
  pinMode(PIN_USER_PTT, INPUT_PULLUP);
  pinMode(PIN_ENC_A, INPUT_PULLUP);
  pinMode(PIN_ENC_B, INPUT_PULLUP);
  pinMode(PIN_ENC_SW, INPUT_PULLUP);

  Serial.begin(115200);
  Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  drawScreen();

  radio.begin(9600, SERIAL_8N1, PIN_RADIO_RX, PIN_RADIO_TX);
  delay(650);
  moduleReady = command("AT+DMOCONNECT", "+DMOCONNECT:0", 600);
  if (moduleReady) {
    moduleReady = configureChannel();
    command("AT+DMOSETVOLUME=5", "+DMOSETVOLUME:0");
  }
  drawScreen();
}

void loop() {
  pollEncoder();
  pollPtt();
  requestRssi();
  if (millis() - lastDrawAt > 500) {
    lastDrawAt = millis();
    drawScreen();
  }
  delay(2);
}