PocketLink UHF — מכשיר קשר חכם עם חוגה
Code for reading and copying
בסוף הצעד: הקוד עולה והמסך אומר RADIO OK
This code is read-only, so it cannot be changed by accident.
#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);
}