diff --git a/examples/SerialTransceiver_nRF52840/SerialTransceiver_nRF52840.ino b/examples/SerialTransceiver_nRF52840/SerialTransceiver_nRF52840.ino new file mode 100644 index 0000000..6ee8ed4 --- /dev/null +++ b/examples/SerialTransceiver_nRF52840/SerialTransceiver_nRF52840.ino @@ -0,0 +1,704 @@ +/* Hamshield + * Example: AppSerialController_nRF52840 + * This is a simple example to demonstrate the HamShield working + * with an Adafruit Feather nRF52840 Express + * + * HamShield to Feather Connections: + * SPKR - Feather A0 + * MIC - Feather D11 + * CLK - Feather D5 + * nCS - Feather D6 + * DAT - Feather D9 + * GND - Feather GND + * VCC - Feather 3.3V + * + * Connect the HamShield to your Feather as above. + * Screw the antenna into the HamShield RF jack. Plug a pair + * of headphones into the HamShield. + * + * Connect the Feather nRF52840 Express to your computer via + * a USB Micro B cable. After uploading this program to + * your Feather, open the Serial Monitor. You should see some + * text displayed that documents the setup process. + * + * Once the Feather is set up and talking to the HamShield, + * you can control it over USB-Serial or BLE-Serial(UART). + * + * Try using Adafruit's Bluefruit app to connect to the Feather. + * Once you're connected, you can control the HamShield using + * the same commands you'd use over USB-Serial. The response to + * all commands will be echoed to both USB-Serial and BLE-Serial(UART). + * + +Commands: + +Mode ASCII Description +-------------- ----------- -------------------------------------------------------------------------------------------------------------------------------------------- +Transmit space Space must be received at least every 500 mS +Receive not space If space is not received and/or 500 mS timeout of space occurs, unit will go into receive mode +Frequency F; Set the receive frequency in KHz, if offset is disabled, this is the transmit frequency +Morse Out M; A small buffer for morse code (32 chars) +Morse In N; Sets mode to Morse In, listening for Morse +Power level P; Set the power amp level, 0 = lowest, 15 = highest +Enable Offset R; 1 turns on repeater offset mode, 0 turns off repeater offset mode +Squelch S; Set the squelch level +TX Offset T; The absolute frequency of the repeater offset to transmit on in KHz +RSSI ? Respond with the current receive level in - dBm (no sign provided on numerical response) +Voice Level ^ Respond with the current voice level (VSSI), only valid when transmitting +DTMF Out D; A small buffer for DTMF out (only 0-9,A,B,C,D,*,# accepted) +DTMF In B; Sets mode to DTMF In, listening for DTMF +PL Tone Tx A; Sets PL tone for TX, value is tone frequency in Hz (float), set to 0 to disable +PL Tone Rx C; Sets PL tone for RX, value is tone frequency in Hz (float), set to 0 to disable +Volume 1 V1; Set volume 1 (value between 0 and 15) +Volume 2 V2; Set volume 2 (value between 0 and 15) +KISS TNC K; Move to KISS TNC mode (send ^; to move back to normal mode). NOT IMPELEMENTED YET +Normal Mode _ Move to Normal mode from any other mode (except TX) + +Responses: + +Condition ASCII Description +------------ ---------- ----------------------------------------------------------------- +Startup *; Startup and shield connection status +Success !; Generic success message for command that returns no value +Error X; Indicates an error code. The numerical value is the type of error +Value :; In response to a query +Status #; Unsolicited status message +Debug Msg @; 32 character debug message +Rx Msg R; up to 32 characters of received message, only if device is in DTMF or Morse Rx modes + +*/ + +// Note that the following are not yet implemented +// TODO: change get_value so it's intuitive +// TODO: Squelch open and squelch shut independently controllable +// TODO: pre/de emph filter +// TODO: walkie-talkie +// TODO: KISS TNC + +#include +#include +#include +#include + +// BLE Service +BLEDis bledis; // device information +BLEUart bleuart; // uart over ble +BLEBas blebas; // battery + + +// create object for radio +HamShield radio(6,5,9); +// To use non-standard pins, use the following initialization +//HamShield radio(ncs_pin, clk_pin, dat_pin); + +#define LED_PIN 3 + +#define MIC_PIN A1 + + +enum {TX, NORMAL, DTMF, MORSE, KISS}; + +int state = NORMAL; +bool rx_ctcss = false; +bool muted = false; + +int txcount = 0; +long timer = 0; // Transmit timer to track timeout (send space to reset) + +long freq = 432100; // 70cm calling frequency, receive frequency and default transmit frequency +long tx_freq = 0; // transmit frequency if repeater is on +int pwr = 0; // tx power + +char cmdbuff[32] = ""; +int temp = 0; + +bool repeater = false; // true if transmit and receive operate on different frequencies +char pl_rx_buffer[32]; // pl tone rx buffer +char pl_tx_buffer[32]; // pl tone tx buffer + +float ctcssin = 0; +float ctcssout = 0; +int cdcssin = 0; +int cdcssout = 0; + +void setup() { + // NOTE: if not using PWM out (MIC pin), it should be held low to avoid tx noise + pinMode(MIC_PIN, OUTPUT); + digitalWrite(MIC_PIN, LOW); + + // initialize serial communication + Serial.begin(115200); + while (!Serial) delay(10); + + // Setup the BLE LED to be enabled on CONNECT + // Note: This is actually the default behaviour, but provided + // here in case you want to control this LED manually via PIN 19 + Bluefruit.autoConnLed(true); + + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + + Bluefruit.begin(); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + Bluefruit.setName("MyBlueHam"); + //Bluefruit.setName(getMcuUniqueID()); // useful testing with multiple central connections + Bluefruit.setConnectCallback(connect_callback); + Bluefruit.setDisconnectCallback(disconnect_callback); + + // Configure and Start Device Information Service + bledis.setManufacturer("Enhanced Radio Devices"); + bledis.setModel("BlueHam"); + bledis.begin(); + + // Configure and Start BLE Uart Service + bleuart.begin(); + + // Start BLE Battery Service + blebas.begin(); + blebas.write(100); + + // Set up and start advertising + startAdv(); + + delay(100); + + SerialWrite(";;;;;;;;;;;;;;;;;;;;;;;;;;"); + + int result = radio.testConnection(); + SerialWrite("*%d;\n", result); + radio.initialize(); // initializes automatically for UHF 12.5kHz channel + radio.frequency(freq); + radio.setVolume1(0xF); + radio.setVolume2(0xF); + radio.setModeReceive(); + radio.setTxSourceMic(); + radio.setRfPower(pwr); + radio.setSQLoThresh(-80); + radio.setSQHiThresh(-70); + radio.setSQOn(); + SerialWrite("*START;\n"); +} + +void startAdv(void) +{ + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + + // Include bleuart 128-bit uuid + Bluefruit.Advertising.addService(bleuart); + + // Secondary Scan Response packet (optional) + // Since there is no room for 'Name' in Advertising packet + Bluefruit.ScanResponse.addName(); + + /* Start Advertising + * - Enable auto advertising if disconnected + * - Interval: fast mode = 20 ms, slow mode = 152.5 ms + * - Timeout for fast mode is 30 seconds + * - Start(timeout) with timeout = 0 will advertise forever (until connected) + * + * For recommended advertising interval + * https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds +} + +void loop() { + // TODO: loop fixing based on serialtransciever! + + char c = 0; + bool ble_serial = false; + if (Serial.available()) { + Serial.readBytes(&c, 1); + } else if (bleuart.available()) { + c = (char) bleuart.read(); + ble_serial = true; + } + + // TODO: BLE + if(c != 0) { + + int text = c; // get the first char to see what the upcoming command is + + switch (state) { + // we handle commands differently based on what state we're in + + case TX: + // we're currently transmitting + // if we got a space, reset our transmit timeout + if(text == ' ') { timer = millis();} + break; + + case NORMAL: + switch(text) { + case ' ': // space - transmit + if(repeater == true && tx_freq != 0) { radio.frequency(tx_freq); } + muted = false; // can't mute (for PL tones) during tx + radio.setUnmute(); + radio.setModeTransmit(); + state = TX; + SerialWrite("#TX,ON;\n"); + timer = millis(); + break; + + case '?': // ? - RSSI + SerialWrite(":%d;\n", radio.readRSSI()); + break; + + case '^': // ^ - VSSI (voice) level + SerialWrite(":%d;\n", radio.readVSSI()); + break; + + case 'F': // F - frequency + getValue(ble_serial); + freq = atol(cmdbuff); + if(radio.frequency(freq) == true) { + SerialWrite("@%d;!;\n", freq); + } else { + SerialWrite("X1;\n"); + } + break; + + case 'P': // P - power level + getValue(ble_serial); + temp = atol(cmdbuff); + radio.setRfPower(temp); + SerialWrite("!;\n"); + break; + + case 'S': // S - squelch + getValue(ble_serial); + temp = atol(cmdbuff); + if (temp < -2 && temp > -130) { + radio.setSQLoThresh(temp); + radio.setSQHiThresh(temp+2); + radio.setSQOn(); + SerialWrite("%d!;\n", temp); + } else { + SerialWrite("X!;\n"); + } + break; + + case 'R': // R - repeater offset mode + getValue(ble_serial); + temp = atol(cmdbuff); + if(temp == 0) { repeater = 0; } + if(temp == 1) { repeater = 1; } + SerialWrite("!;\n"); + break; + + case 'T': // T - transmit offset + getValue(ble_serial); + tx_freq = atol(cmdbuff); + SerialWrite("!;\n"); + break; + + case 'M': // M - Morse + getValue(ble_serial); + if(repeater == true && tx_freq != 0) { radio.frequency(tx_freq); } + muted = false; // can't mute (for PL tones) during tx + radio.setUnmute(); + radio.setModeTransmit(); + delay(300); + radio.morseOut(cmdbuff); + if(repeater == true) { radio.frequency(freq); } + radio.setModeReceive(); + SerialWrite("!;\n"); + break; + + case 'N': // N - set to Morse in Mode + morse_rx_setup(); + state = MORSE; + SerialWrite("!;\n"); + break; + + case 'D': // D - DTMF Out + dtmfSetup(); + getValue(ble_serial); + dtmf_out(cmdbuff); + SerialWrite("!;\n"); + break; + + case 'B': // B - set to DTMF in Mode + dtmfSetup(); + radio.enableDTMFReceive(); + state = DTMF; + SerialWrite("!;\n"); + break; + + case 'A': // A - TX PL Tone configuration command + pl_tone_tx(); + SerialWrite("!;\n"); + break; + + case 'C': // C - RX PL Tone configuration command + pl_tone_rx(); + SerialWrite("!;\n"); + break; + + case 'V': // V - set volume + getValue(ble_serial); + temp = cmdbuff[0]; + if (temp == 0x31) { + temp = atol(cmdbuff + 1); + radio.setVolume1(temp); + SerialWrite("!;\n"); + } else if (temp == 0x32) { + temp = atol(cmdbuff + 1); + radio.setVolume2(temp); + SerialWrite("!;\n"); + } else { + // not a valid volume command, flush buffers + SerialFlush(ble_serial); + SerialWrite("X!;\n"); + } + break; + + case 'K': // K - switch to KISS TNC mode + //state = KISS; + //TODO: set up KISS + SerialWrite("X1;\n"); + break; + + default: + // unknown command, flush the input buffer and wait for next one + SerialWrite("X1;\n"); + SerialFlush(ble_serial); + break; + } + break; + + case KISS: + if ((ble_serial && bleuart.peek() == '_') || (!ble_serial && Serial.peek() == '_')) { + state = NORMAL; + if (rx_ctcss) { + radio.enableCtcss(); + muted = true; // can't mute (for PL tones) during tx + radio.setMute(); + } + } + // TODO: handle KISS TNC + break; + + case MORSE: + if (text == '_') { state = NORMAL; } + if (text == 'M') { // tx message + getValue(ble_serial); + if(repeater == true && tx_freq != 0) { radio.frequency(tx_freq); } + muted = false; // can't mute (for PL tones) during tx + radio.setUnmute(); + radio.setModeTransmit(); + delay(300); + radio.morseOut(cmdbuff); + if(repeater == true) { radio.frequency(freq); } + radio.setModeReceive(); + } else { + // not a valid cmd + SerialFlush(ble_serial); + } + break; + + case DTMF: + if (text == '_') { state = NORMAL; } + if (text == 'D') { // tx message + getValue(ble_serial); + dtmf_out(cmdbuff); + } else { + // not a valid cmd + SerialFlush(ble_serial); + } + break; + + default: + // we're in an invalid state, reset to safe settings + SerialFlush(ble_serial); + radio.frequency(freq); + radio.setModeReceive(); + state = NORMAL; + break; + } + + } + + // now handle any state related functions + switch (state) { + case TX: + if(millis() > (timer + 500)) { + SerialWrite("#TX,OFF;\n"); + radio.setModeReceive(); + if(repeater == true) { radio.frequency(freq); } + if (rx_ctcss) { + radio.setMute(); + muted = true; + } + txcount = 0; + state = NORMAL; + } + break; + + case NORMAL: + // deal with rx ctccs if necessary + if (rx_ctcss) { + if (radio.getCtcssToneDetected()) { + if (muted) { + muted = false; + radio.setUnmute(); + } + } else { + if (!muted) { + muted = true; + radio.setMute(); + } + } + } + break; + + case DTMF: + dtmf_rx(); // wait for DTMF reception + break; + + case MORSE: + morse_rx(); // wait for Morse reception + break; + } + + // get rid of any trailing whitespace in the serial buffer + SerialFlushWhitespace(ble_serial); +} + + +// callback invoked when central connects +void connect_callback(uint16_t conn_handle) +{ + char central_name[32] = { 0 }; + Bluefruit.Gap.getPeerName(conn_handle, central_name, sizeof(central_name)); + + Serial.print("Connected to "); + Serial.println(central_name); +} + +/** + * Callback invoked when a connection is dropped + * @param conn_handle connection where this event happens + * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h + * https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/master/cores/nRF5/nordic/softdevice/s140_nrf52_6.1.1_API/include/ble_hci.h + */ +void disconnect_callback(uint16_t conn_handle, uint8_t reason) +{ + (void) conn_handle; + (void) reason; + + Serial.println(); + Serial.println("Disconnected"); +} + + +void getValue(bool ble_serial) { + int p = 0; + char temp; + + for(;;) { + if((!ble_serial && Serial.available()) || (ble_serial && bleuart.available())) { + if (ble_serial) { + temp = Serial.read(); + } else { + temp = bleuart.read(); + } + if(temp == 59) { + cmdbuff[p] = 0; + return; + } + cmdbuff[p] = temp; + p++; + if(p == 32) { + cmdbuff[0] = 0; + return; + } + } + } +} + +void dtmfSetup() { + radio.setVolume1(6); + radio.setVolume2(0); + radio.setDTMFDetectTime(24); // time to detect a DTMF code, units are 2.5ms + radio.setDTMFIdleTime(50); // time between transmitted DTMF codes, units are 2.5ms + radio.setDTMFTxTime(60); // duration of transmitted DTMF codes, units are 2.5ms +} + +void dtmf_out(char * out_buf) { + if (out_buf[0] == ';' || out_buf[0] == 0) return; // empty message + + uint8_t i = 0; + uint8_t code = radio.DTMFchar2code(out_buf[i]); + + // start transmitting + radio.setDTMFCode(code); // set first + radio.setTxSourceTones(); + if(repeater == true && tx_freq != 0) { radio.frequency(tx_freq); } + muted = false; // can't mute during transmit + radio.setUnmute(); + radio.setModeTransmit(); + delay(300); // wait for TX to come to full power + + bool dtmf_to_tx = true; + while (dtmf_to_tx) { + // wait until ready + while (radio.getDTMFTxActive() != 1) { + // wait until we're ready for a new code + delay(10); + } + if (i < 32 && out_buf[i] != ';' && out_buf[i] != 0) { + code = radio.DTMFchar2code(out_buf[i]); + if (code == 255) code = 0xE; // throw a * in there so we don't break things with an invalid code + radio.setDTMFCode(code); // set first + } else { + dtmf_to_tx = false; + break; + } + i++; + + while (radio.getDTMFTxActive() != 0) { + // wait until this code is done + delay(10); + } + } + // done with tone + radio.setModeReceive(); + if (repeater == true) {radio.frequency(freq);} + radio.setTxSourceMic(); +} + +void dtmf_rx() { + char m = radio.DTMFRxLoop(); + if (m != 0) { + // Note: not doing buffering of messages, + // we just send a single morse character + // whenever we get it + SerialWrite("R%d;\n", m); + } +} + +// TODO: morse config info + +void morse_rx_setup() { + // Set the morse code characteristics + radio.setMorseFreq(MORSE_FREQ); + radio.setMorseDotMillis(MORSE_DOT); + + radio.lookForTone(MORSE_FREQ); + + radio.setupMorseRx(); +} + +void morse_rx() { + char m = radio.morseRxLoop(); + + if (m != 0) { + // Note: not doing buffering of messages, + // we just send a single morse character + // whenever we get it + SerialWrite("R%c;\n",m); + } +} + +void pl_tone_tx() { + memset(pl_tx_buffer,0,32); + uint8_t ptr = 0; + while(1) { + if(Serial.available()) { + uint8_t buf = Serial.read(); + if(buf == 'X') { return; } + if(buf == ';') { pl_tx_buffer[ptr] = 0; program_pl_tx(); return; } + if(ptr == 31) { return; } + pl_tx_buffer[ptr] = buf; ptr++; + } + } +} + +void program_pl_tx() { + float pl_tx = atof(pl_tx_buffer); + radio.setCtcss(pl_tx); + + if (pl_tx == 0) { + radio.disableCtcssTx(); + } else { + radio.enableCtcssTx(); + } +} + +void pl_tone_rx() { + memset(pl_rx_buffer,0,32); + uint8_t ptr = 0; + while(1) { + if(Serial.available()) { + uint8_t buf = Serial.read(); + if(buf == 'X') { return; } + if(buf == ';') { pl_rx_buffer[ptr] = 0; program_pl_rx(); return; } + if(ptr == 31) { return; } + pl_rx_buffer[ptr] = buf; ptr++; + } + } +} + +void program_pl_rx() { + float pl_rx = atof(pl_rx_buffer); + radio.setCtcss(pl_rx); + if (pl_rx == 0) { + rx_ctcss = false; + radio.setUnmute(); + muted = false; + radio.disableCtcssRx(); + } else { + rx_ctcss = true; + radio.setMute(); + muted = true; + radio.enableCtcssRx(); + } +} + +#define TEXT_BUF_LEN 64 +char text_buf[TEXT_BUF_LEN]; +void SerialWrite(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + int str_len = snprintf(text_buf, TEXT_BUF_LEN, fmt, args); + va_end(args); + + bleuart.write(text_buf, str_len); + Serial.write(text_buf, str_len); +} + +void SerialFlush(bool ble_serial) { + if (ble_serial) { + while (bleuart.available()) { bleuart.read(); } + } else { + while (Serial.available()) { Serial.read(); } + } +} + + +void SerialFlushWhitespace(bool ble_serial) { + if (!ble_serial && Serial.available()) { + char cpeek = Serial.peek(); + while (cpeek == ' ' || cpeek == '\r' || cpeek == '\n') + { + Serial.read(); + cpeek = Serial.peek(); + } + } else if (ble_serial && bleuart.available()) { + char cpeek = bleuart.peek(); + while (cpeek == ' ' || cpeek == '\r' || cpeek == '\n') + { + bleuart.read(); + cpeek = bleuart.peek(); + } + } +} \ No newline at end of file