├── Arduino_cos_repeater.ino ├── Arduino_vox_repeater.ino ├── LICENSE └── README.md /Arduino_cos_repeater.ino: -------------------------------------------------------------------------------- 1 | //COS sensing bi-directional repeater for Arduino 2 | //Matthew Miller 3 | //KK4NDE 4 | //17-Sept-2016 5 | // 6 | //Changelog: 7 | //cos_repeater_generic - Matthew Miller 17 September 2016 8 | // Initial version 9 | //cos_repeater_generic2 - Matthew Miller 10 December 2016 10 | // Bugfix - changed COS function for clarity to match wiring 11 | // Debug - added Serial deugging enabled with preprocessor flag 12 | //cos_repeater_generic3 - Matthew Miller 10 December 2016 13 | // Added flag to only ID if radio transmitted 14 | // Cleaned up some code 15 | //cos_repeater_generic4 - Matthew Miller 17 December 2016 16 | // Added software-flag to disable a radio 17 | // Fixed battMon flag changed int to boolean 18 | 19 | //This is based on the VOX repeater sketch but modified for more reliable use 20 | //by interfaceing radios equipped with carrier-operated squealch output. 21 | 22 | //I have provided example pin numbers that I think would work with the Arduino Uno, 23 | //they can be changed for other boards or ATTINY chips depending on your need. 24 | //configure the #define at the beginning of the code 25 | //configure the radioA.value radioB.value lines in the setup() function 26 | //NOTE: voltSensePin, radioA.cosPin, and radioB.cosPin should be DIGITAL inputs 27 | //if you don't want to use the voltSensePin just tie that pin to +V and it will always assume the battery is full 28 | 29 | //Callsign to ID with, use lowercase only 30 | #define CALLSIGN "xx0xxx" 31 | 32 | //Sets the COS (Carrier Operated Squealch) value to represent "squealch open" 33 | //If your COS is +V when carrier present/squealch open, use HIGH 34 | //If your COS is 0V when carrier present/squealch open, use LOW 35 | #define COS_VALUE_SQL_OPEN LOW 36 | 37 | //Analog pin for voltage sense 38 | #define voltSensePin 0 39 | 40 | //define threshold for low battery 41 | #define lowBattThreshold 11.5 42 | 43 | //define threshold below which low battery is not triggered 44 | //this lets you power it with low-voltage and not get any alerts 45 | #define lowNotifyFloor 9.5 46 | 47 | //how many milliseconds to ID every 48 | //600000 is 10 minutes in milliseconds 49 | #define idTimeout 600000 50 | #define onlyIdAfterTx true 51 | 52 | //define delay for squealch-tail hold over in milliseconds 53 | #define cosDelay 1000 54 | 55 | //morse code "dit" base unit length in milliseconds 56 | #define ditLen 60 57 | 58 | //the pitch for the dit/dah tone used by the ID 59 | #define tonePitch 800 60 | 61 | //uncomment to enable serial debugging 62 | //#define ENABLE_DEBUG 63 | //#define ENABLE_DEBUG_COS_STATE 64 | //#define ENABLE_DEBUG_PTT 65 | //#define ENABLE_DEBUG_RADIO_STATE 66 | //#define ENABLE_DEBUG_NEEDSID 67 | 68 | //data structure for radio info 69 | struct Radio 70 | { 71 | //cosPin - Audio In (for COS) - Digital Pin 72 | //micPin - MIC Mix (for tone) - Digital Pin 73 | //pttPin - PTT OUT (for TX) - Digital Pin 74 | int micPin, pttPin, cosPin, autoId; 75 | long lastBattMonTime=idTimeout, 76 | lastIdTime=idTimeout, 77 | lastCosTime=0; 78 | boolean battMon=true, 79 | needsId=true, 80 | txAllowed=true; 81 | #ifdef ENABLE_DEBUG 82 | String nametag; 83 | #endif 84 | }; 85 | 86 | //globals to store radio config 87 | Radio radioA, radioB; 88 | 89 | //declarations for functions which use radio struct 90 | void configure(Radio &radio); 91 | void cosCheckAndRepeat(Radio &rxRadio, Radio &txRadio); 92 | void txAutoId(Radio &radio); 93 | void lowBattCheck(Radio &radio); 94 | boolean isBusy(Radio &radio); 95 | 96 | //some declarations for debugging 97 | #ifdef ENABLE_DEBUG 98 | void printRadioState(Radio &radio); 99 | long debugRadioStateLastTime=0; 100 | #endif 101 | 102 | void setup() { 103 | Serial.begin(9600); 104 | 105 | /****************************************************** 106 | ** Configure radio connection pins and settings here ** 107 | ******************************************************/ 108 | 109 | //set config for radio A 110 | //Note, these would all be "digital" pins 111 | radioA.cosPin=2; 112 | radioA.micPin=3; 113 | radioA.pttPin=4; 114 | radioA.autoId=true; 115 | radioA.battMon=false; 116 | radioA.txAllowed=true; 117 | //for debugging only 118 | #ifdef ENABLE_DEBUG 119 | radioA.nametag="radioA"; 120 | #endif 121 | 122 | //set config for radio B 123 | //Note, these would all be "digital" pins 124 | radioB.cosPin=5; 125 | radioB.micPin=6; 126 | radioB.pttPin=7; 127 | radioB.autoId=true; 128 | radioB.battMon=false; 129 | radioB.txAllowed=true; 130 | //for debugging only 131 | #ifdef ENABLE_DEBUG 132 | radioB.nametag="radioB"; 133 | #endif 134 | 135 | //apply config for radios (set pinmode/etc) 136 | configure(radioA); 137 | configure(radioB); 138 | 139 | //broadcast ID if applicable 140 | txAutoId(radioA); 141 | txAutoId(radioB); 142 | 143 | #ifdef ENABLE_DEBUG 144 | printSetupDebug(); 145 | #endif 146 | } 147 | 148 | //configures pinmode and setup for radio 149 | void configure(Radio &radio) 150 | { 151 | #ifdef ENABLE_DEBUG 152 | Serial.print("Configuring "); 153 | Serial.println(radio.nametag); 154 | #endif 155 | pinMode(radio.micPin,OUTPUT); 156 | pinMode(radio.pttPin,OUTPUT); 157 | digitalWrite(radio.micPin,LOW); 158 | digitalWrite(radio.pttPin,LOW); 159 | } 160 | 161 | void loop() 162 | { 163 | if(isEnabled(radioA.txAllowed) && !isBusy(radioB)) //if the other radio is transmitting, this one must be receiving so don't key up 164 | { 165 | lowBattCheck(radioA); 166 | txAutoId(radioA); 167 | cosCheckAndRepeat(radioB,radioA); 168 | } 169 | 170 | if(isEnabled(radioB.txAllowed) && !isBusy(radioA)) //if the other radio is transmitting, this one must be receiving so don't key up 171 | { 172 | lowBattCheck(radioB); 173 | txAutoId(radioB); 174 | cosCheckAndRepeat(radioA,radioB); 175 | } 176 | 177 | #ifdef ENABLE_DEBUG_RADIO_STATE 178 | if(millis()-debugRadioStateLastTime > 10000) 179 | { 180 | printAllRadioState(); 181 | debugRadioStateLastTime=millis(); 182 | } 183 | #endif 184 | } 185 | 186 | //checks if a radio's PTT pin is keyed 187 | boolean isBusy(Radio &radio) 188 | { 189 | boolean isBusy = digitalRead(radio.pttPin); 190 | #ifdef ENABLE_DEBUG_VERBOSE 191 | Serial.print(radio.nametag); 192 | Serial.print(" isBusy"); 193 | Serial.print("="); 194 | Serial.println(isBusy); 195 | #endif 196 | return isBusy; 197 | } 198 | 199 | //checks if feature is enabled (if pin is true/false) 200 | // In the future this had some grand plan of making it 201 | // take a pin # and then check if that pin was high 202 | // or low depending on jumpers/switches but that didn't 203 | // happen so now this just returns the feature-boolean 204 | // stored in the radio object. 205 | boolean isEnabled(boolean feature) 206 | { 207 | return feature; //temp just return true/false coded 208 | //return digitalRead(pin); 209 | } 210 | 211 | //trigger PTT based on COS input and delay 212 | void cosCheckAndRepeat(Radio &rxRadio, Radio &txRadio) 213 | { 214 | if(isEnabled(txRadio.txAllowed)) 215 | { 216 | // test if the pin has cos 217 | if(digitalRead(rxRadio.cosPin) == COS_VALUE_SQL_OPEN) 218 | { 219 | #ifdef ENABLE_DEBUG_COS_STATE 220 | Serial.print("COS squealch open on "); 221 | Serial.println(rxRadio.nametag); 222 | #endif 223 | 224 | //cos active 225 | #ifdef ENABLE_DEBUG_PTT 226 | Serial.print("Turning on TX for "); 227 | Serial.println(txRadio.nametag); 228 | #endif 229 | digitalWrite(txRadio.pttPin,HIGH); 230 | #ifdef ENABLE_DEBUG_NEEDSID 231 | Serial.print(txRadio.nametag); 232 | Serial.println("setting needsId=true in 'cos active' if clause"); 233 | #endif 234 | txRadio.needsId=true; 235 | rxRadio.lastCosTime=millis(); 236 | } 237 | else 238 | { 239 | if(millis()-rxRadio.lastCosTime < cosDelay) 240 | { 241 | //cos delay 242 | #ifdef ENABLE_DEBUG_NEEDSID 243 | Serial.print(txRadio.nametag); 244 | Serial.println("setting needsId=true in 'cos delay' else-if clause"); 245 | #endif 246 | txRadio.needsId=true; 247 | } 248 | else 249 | { 250 | #ifdef ENABLE_DEBUG_PTT 251 | Serial.print("Turning off TX for "); 252 | Serial.println(txRadio.nametag); 253 | #endif 254 | digitalWrite(txRadio.pttPin,LOW); 255 | } 256 | } 257 | } 258 | } 259 | 260 | //broadcast ID if applicable 261 | void txAutoId(Radio &radio) 262 | { 263 | //If we're only suppsoed to ID after TX 264 | //and we didn't TX 265 | if(onlyIdAfterTx && !radio.needsId) 266 | { 267 | //keep resetting the last ID time to now 268 | radio.lastIdTime=millis(); 269 | } 270 | 271 | if(isEnabled(radio.txAllowed) && isEnabled(radio.autoId) && (millis()-radio.lastIdTime) > idTimeout) 272 | { 273 | #ifdef ENABLE_DEBUG 274 | Serial.print("Sending autoID on "); 275 | Serial.println(radio.nametag); 276 | #endif 277 | 278 | boolean tx=digitalRead(radio.pttPin); 279 | digitalWrite(radio.pttPin,HIGH); 280 | delay(500); 281 | morseCode(radio.micPin,CALLSIGN); 282 | #ifdef ENABLE_DEBUG_NEEDSID 283 | Serial.print(radio.nametag); 284 | Serial.println(" txAutoId setting needsId=false"); 285 | #endif 286 | radio.needsId=false; 287 | radio.lastIdTime=millis(); 288 | digitalWrite(radio.pttPin,tx); 289 | } 290 | } 291 | 292 | //broadcast low battery if applicable 293 | void lowBattCheck(Radio &radio) 294 | { 295 | float voltage=getPowerVoltage(voltSensePin); 296 | if(isEnabled(radio.txAllowed) && isEnabled(radio.battMon) && voltage < lowBattThreshold && voltage > lowNotifyFloor && (millis()-radio.lastBattMonTime) > idTimeout) 297 | { 298 | #ifdef ENABLE_DEBUG 299 | Serial.print("Sending low-battery on "); 300 | Serial.println(radio.nametag); 301 | #endif 302 | 303 | boolean tx=digitalRead(radio.pttPin); 304 | digitalWrite(radio.pttPin,HIGH); 305 | radio.needsId=true; 306 | delay(500); 307 | 308 | //encode low battery morse code message 309 | // char temp[]="lb "; 310 | // strcat(temp,voltage); 311 | // strcat(temp,"v"); 312 | // morseCode(radio.micPin,temp); 313 | morseCode(radio.micPin,"lb"); 314 | //morseCode(radio.micPin,"lb "+ toString(voltage) + "v"); 315 | 316 | radio.lastBattMonTime=millis(); 317 | digitalWrite(radio.pttPin,tx); 318 | radio.needsId=true; 319 | } 320 | } 321 | 322 | //for floats from ~1 to ~19 323 | void strcat(char * appendTo, float input) 324 | { 325 | char temp[]="x"; 326 | if(input > 10) 327 | { 328 | strcat(appendTo,"1"); 329 | input-=10; 330 | } 331 | temp[0]=(char)((int)input+48); //add 48 to shift to ascii value 332 | strcat(appendTo,temp); //append to output 333 | input-=(int)input; //take off whole value 334 | strcat(appendTo,"."); 335 | input*=10; //iterate to first place past decimal 336 | if((input-(int)input)>.4) //round (because here we will drop everything after decimal) 337 | input++; 338 | temp[0]=(char)((int)input+48); //add 48 to shift to ascii value 339 | strcat(appendTo,temp); //append to output 340 | } 341 | 342 | //send morse code message by applying tone to codePin 343 | void morseCode(int codePin, char* message) 344 | { 345 | // message.trim(); 346 | //message.toLowerCase(); 347 | int code; 348 | int length; 349 | for(unsigned int x=0; x < strlen(message); x++) 350 | { 351 | //shift case 352 | if(message[x] < 91 && message[x] > 64) 353 | message[x]+=32; 354 | 355 | //encode morse code 356 | switch(message[x]) 357 | { 358 | case 'a': 359 | //strcpy(temp,".-"); 360 | code=B01; 361 | length=2; 362 | break; 363 | case 'b': 364 | //strcpy(temp,"-..."); 365 | code=B1000; 366 | length=4; 367 | break; 368 | case 'c': 369 | //strcpy(temp,"-.-."); 370 | code=B1010; 371 | length=4; 372 | break; 373 | case 'd': 374 | //strcpy(temp,"-.."); 375 | code=B100; 376 | length=3; 377 | break; 378 | case 'e': 379 | //strcpy(temp,"."); 380 | code=B0; 381 | length=1; 382 | break; 383 | case 'f': 384 | //strcpy(temp,"..-."); 385 | code=B0010; 386 | length=4; 387 | break; 388 | case 'g': 389 | //strcpy(temp,"--."); 390 | code=B110; 391 | length=3; 392 | break; 393 | case 'h': 394 | //strcpy(temp,"...."); 395 | code=B0000; 396 | length=4; 397 | break; 398 | case 'i': 399 | //strcpy(temp,".."); 400 | code=B00; 401 | length=2; 402 | break; 403 | case 'j': 404 | //strcpy(temp,".---"); 405 | code=B0111; 406 | length=4; 407 | break; 408 | case 'k': 409 | //strcpy(temp,"-.-"); 410 | code=B101; 411 | length=3; 412 | break; 413 | case 'l': 414 | //strcpy(temp,".-.."); 415 | code=B0100; 416 | length=4; 417 | break; 418 | case 'm': 419 | //strcpy(temp,"--"); 420 | code=B11; 421 | length=2; 422 | break; 423 | case 'n': 424 | //strcpy(temp,"-."); 425 | code=B10; 426 | length=2; 427 | break; 428 | case 'o': 429 | //strcpy(temp,"---"); 430 | code=B111; 431 | length=3; 432 | break; 433 | case 'p': 434 | //strcpy(temp,".--."); 435 | code=B0110; 436 | length=4; 437 | break; 438 | case 'q': 439 | //strcpy(temp,"--.-"); 440 | code=B1101; 441 | length=4; 442 | break; 443 | case 'r': 444 | //strcpy(temp,".-."); 445 | code=B010; 446 | length=3; 447 | break; 448 | case 's': 449 | //strcpy(temp,"..."); 450 | code=B000; 451 | length=3; 452 | break; 453 | case 't': 454 | //strcpy(temp,"-"); 455 | code=B1; 456 | length=1; 457 | break; 458 | case 'u': 459 | //strcpy(temp,"..-"); 460 | code=B001; 461 | length=3; 462 | break; 463 | case 'v': 464 | //strcpy(temp,"...-"); 465 | code=B0001; 466 | length=4; 467 | break; 468 | case 'w': 469 | //strcpy(temp,".--"); 470 | code=B011; 471 | length=3; 472 | break; 473 | case 'x': 474 | //strcpy(temp,"-..-"); 475 | code=B1001; 476 | length=4; 477 | break; 478 | case 'y': 479 | //strcpy(temp,"-.--"); 480 | code=B1011; 481 | length=4; 482 | break; 483 | case 'z': 484 | //strcpy(temp,"--.."); 485 | code=B1100; 486 | length=4; 487 | break; 488 | case '0': 489 | //strcpy(temp,"-----"); 490 | code=B11111; 491 | length=5; 492 | break; 493 | case '1': 494 | //strcpy(temp,".----"); 495 | code=B01111; 496 | length=5; 497 | break; 498 | case '2': 499 | //strcpy(temp,"..---"); 500 | code=B00111; 501 | length=5; 502 | break; 503 | case '3': 504 | //strcpy(temp,"...--"); 505 | code=B00011; 506 | length=5; 507 | break; 508 | case '4': 509 | //strcpy(temp,"....-"); 510 | code=B00001; 511 | length=5; 512 | break; 513 | case '5': 514 | //strcpy(temp,"....."); 515 | code=B00000; 516 | length=5; 517 | break; 518 | case '6': 519 | //strcpy(temp,"-...."); 520 | code=B10000; 521 | length=5; 522 | break; 523 | case '7': 524 | //strcpy(temp,"--..."); 525 | code=B11000; 526 | length=5; 527 | break; 528 | case '8': 529 | //strcpy(temp,"---.."); 530 | code=B11100; 531 | length=5; 532 | break; 533 | case '9': 534 | //strcpy(temp,"----."); 535 | code=B11110; 536 | length=5; 537 | break; 538 | case ' ': 539 | //strcpy(temp,""); 540 | code=B0; 541 | length=0; 542 | delay(7*ditLen); 543 | break; 544 | case '.': 545 | //strcpy(temp,".-.-.-"); 546 | code=B010101; 547 | length=6; 548 | break; 549 | case '/': 550 | //strcpy(temp,"-..-."); 551 | code=B10010; 552 | length=5; 553 | break; 554 | case '-': 555 | //strcpy(temp,"-....-"); 556 | code=B100001; 557 | length=6; 558 | break; 559 | case '?': 560 | //strcpy(temp,"..--.."); 561 | code=B001100; 562 | length=6; 563 | break; 564 | case '@': //debug symbol 565 | code=0; 566 | length=0; 567 | break; 568 | case '%': //debug symbol 569 | code=0; 570 | length=0; 571 | break; 572 | default: 573 | //strcpy(temp,""); 574 | code=B0; 575 | length=0; 576 | break; 577 | } 578 | 579 | while(length > 0) 580 | { 581 | //determine if it's a dit or a dot 582 | if(code & bitMask(length)) 583 | { 584 | //1 is a dit 585 | tone(codePin,tonePitch); 586 | delay(3*ditLen); 587 | noTone(codePin); 588 | delay(ditLen); 589 | } 590 | else 591 | { 592 | //0 is a dot 593 | tone(codePin,tonePitch); 594 | delay(ditLen); 595 | noTone(codePin); 596 | delay(ditLen); 597 | } 598 | length--; 599 | } 600 | delay(ditLen); 601 | } 602 | } 603 | 604 | //generates a "bit mask" for getting nth bit from int 605 | int bitMask(int bitNumber) 606 | { 607 | int value=1; 608 | for(int x=1; x < bitNumber; x++) 609 | value*=2; 610 | return value; 611 | } 612 | 613 | //gets voltage from analog input pin 614 | float getPowerVoltage(int pin) 615 | { 616 | // R1 = 2200 (Vin to midpoint) 617 | // R2 = 1000 (midpoint to gnd) 618 | // put 5.1v protectioin zener in parallel for R2 to protect arduino input against overvolt 619 | // formula: 620 | // ( value/1023 * (arduino vcc) ) / ( R2 / (R2 + R1) ) + (Vin diode drop) 621 | //return ((analogRead(pin)/1023.0)*5.0) / (1000.0 / (2200.0 + 1000.0)) + 0.76 ; 622 | return (analogRead(pin)*0.0156402737047898) + 0.76; //simplified 623 | } 624 | 625 | 626 | #ifdef ENABLE_DEBUG 627 | void printSetupDebug() 628 | { 629 | Serial.println("------ setup() complete ------"); 630 | printProgramConfig(); 631 | Serial.println(""); 632 | printRadioState(radioA); 633 | Serial.println(""); 634 | printRadioState(radioB); 635 | Serial.println("------------------------------"); 636 | debugRadioStateLastTime=millis(); 637 | } 638 | 639 | void printAllRadioState() 640 | { 641 | Serial.println("------ radio state ------"); 642 | Serial.print("millis="); 643 | Serial.println(millis()); 644 | Serial.println(""); 645 | printRadioState(radioA); 646 | Serial.println(""); 647 | printRadioState(radioB); 648 | Serial.println("-------------------------"); 649 | } 650 | 651 | void printProgramConfig() 652 | { 653 | Serial.print("CALLSIGN="); 654 | Serial.println(CALLSIGN); 655 | Serial.print("COS_VALUE_SQL_OPEN="); 656 | Serial.println(COS_VALUE_SQL_OPEN); 657 | Serial.print("voltSensePin="); 658 | Serial.println(voltSensePin); 659 | Serial.print("lowBattThreshold="); 660 | Serial.println(lowBattThreshold); 661 | Serial.print("lowNotifyFloor="); 662 | Serial.println(lowNotifyFloor); 663 | Serial.print("idTimeout="); 664 | Serial.print(idTimeout); 665 | float idTimeoutSec=((float)idTimeout/1000); 666 | Serial.print(" ("); 667 | Serial.print(idTimeoutSec); 668 | Serial.println(" seconds)"); 669 | Serial.print("cosDelay="); 670 | Serial.print("cosDelay"); 671 | Serial.println("ms"); 672 | Serial.print("ditLen="); 673 | Serial.println(ditLen); 674 | Serial.print("tonePitch"); 675 | Serial.println(tonePitch); 676 | } 677 | 678 | void printRadioState(Radio &radio) 679 | { 680 | //cosPin - Audio In (for COS) - Digital Pin 681 | //micPin - MIC Mix (for tone) - Digital Pin 682 | //pttPin - PTT OUT (for TX) - Digital Pin 683 | Serial.print("Radio state debug: "); 684 | Serial.println(radio.nametag); 685 | Serial.print("memAddr="); 686 | Serial.println(reinterpret_cast(&radio)); 687 | Serial.print("micPin="); 688 | Serial.println(radio.micPin); 689 | Serial.print("pttPin="); 690 | Serial.println(radio.pttPin); 691 | Serial.print("cosPin="); 692 | Serial.println(radio.cosPin); 693 | Serial.print("autoId="); 694 | Serial.println(radio.autoId); 695 | Serial.print("battMon="); 696 | Serial.println(radio.battMon); 697 | Serial.print("lastBattMonTime="); 698 | Serial.println(radio.lastBattMonTime); 699 | Serial.print("lastIdTime="); 700 | Serial.println(radio.lastIdTime); 701 | Serial.print("lastCosTime="); 702 | Serial.println(radio.lastCosTime); 703 | Serial.print("needsId="); 704 | Serial.println(radio.needsId); 705 | Serial.print("txAllowed="); 706 | Serial.println(radio.txAllowed); 707 | } 708 | #endif 709 | -------------------------------------------------------------------------------- /Arduino_vox_repeater.ino: -------------------------------------------------------------------------------- 1 | //VOX sensing bi-directional repeater for Arduino 2 | //Matthew Miller 3 | //KK4NDE 4 | //24-March-2013 5 | 6 | //Here are some common pins you may want to use depending on if you have an Arduino or ATTINY 7 | //configure the #define at the beginning of the code 8 | //configure the radioA.value radioB.value lines in the setup() function 9 | //NOTE: voltSensePin, radioA.voxPin, and radioB.voxPin should be ANALOG inputs 10 | //if you don't want to use the voltSensePin just tie that pin to +V and it will always assume the battery is full 11 | /* 12 | Arduino pins 13 | #define voltSensePin 2 14 | //set config for radio A 15 | radioA.voxPin=0; 16 | radioA.micPin=2; 17 | radioA.pttPin=3; 18 | radioA.autoId=true; 19 | radioA.battMon=true; 20 | radioA.lastBattMonTime=idTimeout; 21 | radioA.lastIdTime=idTimeout; 22 | radioA.lastVoxTime=0; 23 | 24 | //set config for radio B 25 | radioB.voxPin=1; 26 | radioB.micPin=12; 27 | radioB.pttPin=13; 28 | radioB.autoId=false; 29 | radioB.battMon=false; 30 | radioB.lastBattMonTime=idTimeout; 31 | radioB.lastIdTime=idTimeout; 32 | radioB.lastVoxTime=0; 33 | 34 | ATTiny84 pins 35 | #define voltSensePin 4 36 | //set config for radio A 37 | radioA.voxPin=2; 38 | radioA.micPin=7; 39 | radioA.pttPin=0; 40 | radioA.autoId=true; //0 41 | radioA.battMon=false; //1 42 | radioA.lastBattMonTime=idTimeout; 43 | radioA.lastIdTime=idTimeout; 44 | radioA.lastVoxTime=0; 45 | 46 | //set config for radio B 47 | radioB.voxPin=1; 48 | radioB.micPin=10; 49 | radioB.pttPin=1; 50 | radioB.autoId=true; //2 51 | radioB.battMon=true; //3 52 | radioB.lastBattMonTime=idTimeout; 53 | radioB.lastIdTime=idTimeout; 54 | radioB.lastVoxTime=0; 55 | */ 56 | 57 | //Callsign to ID with, use lowercase only 58 | #define CALLSIGN "xx0xxx" 59 | 60 | //Analog pin for voltage sense 61 | #define voltSensePin 6 62 | 63 | //define threshold for low battery 64 | #define lowBattThreshold 11.5 65 | 66 | //define threshold below which low battery is not triggered 67 | #define lowNotifyFloor 9.5 68 | 69 | //how many milliseconds to ID every 70 | //600000 is 10 minutes in milliseconds 71 | #define idTimeout 600000 72 | 73 | //define input value for no-audio 74 | //this should probably be near 512 but will vary depending on your voltage devider 75 | //that is used for buffering the voxPin input 76 | #define normVal 511 77 | 78 | //define deviation to activate VOX 79 | //smaller values activate sooner, larger values require more audio variation to trigger 80 | #define devVal 4 81 | 82 | //define delay for VOX hold over in milliseconds 83 | #define voxDelay 1000 84 | 85 | //morse code "dit" base unit length in milliseconds 86 | #define ditLen 60 87 | 88 | //the pitch for the dit/dah tone used by the ID 89 | #define tonePitch 800 90 | 91 | //data structure for radio info 92 | struct Radio 93 | { 94 | //voxPin - Audio In (for VOX) - Analog Pin 95 | //micPin - MIC Mix (for tone) - Digital Pin 96 | //pttPin - PTT OUT (for TX) - Digital Pin 97 | int micPin, pttPin, voxPin, autoId, battMon; 98 | long lastBattMonTime, lastIdTime, lastVoxTime; 99 | }; 100 | 101 | //globals to store radio config 102 | Radio radioA, radioB; 103 | 104 | //declarations for functions which use radio struct 105 | void configure(Radio &radio); 106 | void vox(Radio &radio); 107 | void txAutoId(Radio &radio); 108 | void lowBattCheck(Radio &radio); 109 | boolean isBusy(Radio &radio); 110 | 111 | void setup() { 112 | 113 | /****************************************************** 114 | ** Configure radio connection pins and settings here ** 115 | ******************************************************/ 116 | 117 | //set config for radio A 118 | radioA.voxPin=2; 119 | radioA.micPin=7; 120 | radioA.pttPin=0; 121 | radioA.autoId=true; 122 | radioA.battMon=false; 123 | radioA.lastBattMonTime=idTimeout; 124 | radioA.lastIdTime=idTimeout; 125 | radioA.lastVoxTime=0; 126 | 127 | //set config for radio B 128 | radioB.voxPin=1; 129 | radioB.micPin=10; 130 | radioB.pttPin=1; 131 | radioB.autoId=true; 132 | radioB.battMon=true; 133 | radioB.lastBattMonTime=idTimeout; 134 | radioB.lastIdTime=idTimeout; 135 | radioB.lastVoxTime=0; 136 | 137 | //apply config for radios (set pinmode/etc) 138 | configure(radioA); 139 | configure(radioB); 140 | 141 | //This line is for debugging on ATTINY without eating memory and resources nor 142 | //requiring a serial console. Use Serial.print for Arduino Uno instead. 143 | //When uncommented, it will use the radioA output to beep out the values 144 | //devided by 4 that are detected on voxPin on radioA followed by voxPin on radioB 145 | //encoded in binary -- such that dit is 0 dah is 1 146 | //the value is one byte long and should be multiplied by 4 to get the proper value 147 | //to set for the #define normValue 148 | //example, ..------..-----. 149 | // 0111111110000000 150 | // \__127_/\__128_/ 151 | // x4=508 x4=512 152 | // radioA "no sound" vox pin value is about 508 153 | // radioB "no sound" vox pin is about 512 154 | 155 | //morseCode(radioA.micPin,"@%"); 156 | 157 | //broadcast ID if applicable 158 | txAutoId(radioA); 159 | txAutoId(radioB); 160 | } 161 | 162 | //configures pinmode and setup for radio 163 | void configure(Radio &radio) 164 | { 165 | pinMode(radio.micPin,OUTPUT); 166 | pinMode(radio.pttPin,OUTPUT); 167 | digitalWrite(radio.micPin,LOW); 168 | digitalWrite(radio.pttPin,LOW); 169 | } 170 | 171 | void loop() 172 | { 173 | if(!isBusy(radioB)) //if the other radio is transmitting, this one must be receiving so don't key up 174 | { 175 | lowBattCheck(radioA); 176 | txAutoId(radioA); 177 | vox(radioA); 178 | } 179 | 180 | if(!isBusy(radioA)) //if the other radio is transmitting, this one must be receiving so don't key up 181 | { 182 | lowBattCheck(radioB); 183 | txAutoId(radioB); 184 | vox(radioB); 185 | } 186 | } 187 | 188 | //checks if a radio's PTT pin is keyed 189 | boolean isBusy(Radio &radio) 190 | { 191 | return digitalRead(radio.pttPin); 192 | } 193 | 194 | //checks if feature is enabled (if pin is true/false) 195 | boolean isEnabled(int pin) 196 | { 197 | return pin; //temp just return true/false coded 198 | //return digitalRead(pin); 199 | } 200 | 201 | //trigger PTT based on VOX input and delay 202 | void vox(Radio &radio) 203 | { 204 | // read the input on analog pins 205 | int voxVal = analogRead(radio.voxPin); 206 | 207 | // test if the pin has audio 208 | if(voxVal>(normVal+devVal) || voxVal<(normVal-devVal)) 209 | { 210 | //vox active 211 | digitalWrite(radio.pttPin,HIGH); 212 | radio.lastVoxTime=millis(); 213 | } 214 | else 215 | { 216 | if(millis()-radio.lastVoxTime < voxDelay) 217 | { 218 | //vox delay 219 | } 220 | else 221 | { 222 | digitalWrite(radio.pttPin,LOW); 223 | } 224 | } 225 | } 226 | 227 | //broadcast ID if applicable 228 | void txAutoId(Radio &radio) 229 | { 230 | if(isEnabled(radio.autoId) && (millis()-radio.lastIdTime) > idTimeout) 231 | { 232 | boolean tx=digitalRead(radio.pttPin); 233 | digitalWrite(radio.pttPin,HIGH); 234 | delay(500); 235 | morseCode(radio.micPin,CALLSIGN); 236 | radio.lastIdTime=millis(); 237 | digitalWrite(radio.pttPin,tx); 238 | } 239 | } 240 | 241 | //broadcast low battery if applicable 242 | void lowBattCheck(Radio &radio) 243 | { 244 | float voltage=getPowerVoltage(voltSensePin); 245 | if(isEnabled(radio.battMon) && voltage < lowBattThreshold && voltage > lowNotifyFloor && (millis()-radio.lastBattMonTime) > idTimeout) 246 | { 247 | boolean tx=digitalRead(radio.pttPin); 248 | digitalWrite(radio.pttPin,HIGH); 249 | delay(500); 250 | 251 | //encode low battery morse code message 252 | // char temp[]="lb "; 253 | // strcat(temp,voltage); 254 | // strcat(temp,"v"); 255 | // morseCode(radio.micPin,temp); 256 | morseCode(radio.micPin,"lb"); 257 | //morseCode(radio.micPin,"lb "+ toString(voltage) + "v"); 258 | 259 | radio.lastBattMonTime=millis(); 260 | digitalWrite(radio.pttPin,tx); 261 | } 262 | } 263 | 264 | //for floats from ~1 to ~19 265 | void strcat(char * appendTo, float input) 266 | { 267 | char temp[]="x"; 268 | if(input > 10) 269 | { 270 | strcat(appendTo,"1"); 271 | input-=10; 272 | } 273 | temp[0]=(char)((int)input+48); //add 48 to shift to ascii value 274 | strcat(appendTo,temp); //append to output 275 | input-=(int)input; //take off whole value 276 | strcat(appendTo,"."); 277 | input*=10; //iterate to first place past decimal 278 | if((input-(int)input)>.4) //round (because here we will drop everything after decimal) 279 | input++; 280 | temp[0]=(char)((int)input+48); //add 48 to shift to ascii value 281 | strcat(appendTo,temp); //append to output 282 | } 283 | 284 | //send morse code message by applying tone to codePin 285 | void morseCode(int codePin, char* message) 286 | { 287 | // message.trim(); 288 | //message.toLowerCase(); 289 | int code; 290 | int length; 291 | for(unsigned int x=0; x < strlen(message); x++) 292 | { 293 | //shift case 294 | if(message[x] < 91 && message[x] > 64) 295 | message[x]+=32; 296 | 297 | //encode morse code 298 | switch(message[x]) 299 | { 300 | case 'a': 301 | //strcpy(temp,".-"); 302 | code=B01; 303 | length=2; 304 | break; 305 | case 'b': 306 | //strcpy(temp,"-..."); 307 | code=B1000; 308 | length=4; 309 | break; 310 | case 'c': 311 | //strcpy(temp,"-.-."); 312 | code=B1010; 313 | length=4; 314 | break; 315 | case 'd': 316 | //strcpy(temp,"-.."); 317 | code=B100; 318 | length=3; 319 | break; 320 | case 'e': 321 | //strcpy(temp,"."); 322 | code=B0; 323 | length=1; 324 | break; 325 | case 'f': 326 | //strcpy(temp,"..-."); 327 | code=B0010; 328 | length=4; 329 | break; 330 | case 'g': 331 | //strcpy(temp,"--."); 332 | code=B110; 333 | length=3; 334 | break; 335 | case 'h': 336 | //strcpy(temp,"...."); 337 | code=B0000; 338 | length=4; 339 | break; 340 | case 'i': 341 | //strcpy(temp,".."); 342 | code=B00; 343 | length=2; 344 | break; 345 | case 'j': 346 | //strcpy(temp,".---"); 347 | code=B0111; 348 | length=4; 349 | break; 350 | case 'k': 351 | //strcpy(temp,"-.-"); 352 | code=B101; 353 | length=3; 354 | break; 355 | case 'l': 356 | //strcpy(temp,".-.."); 357 | code=B0100; 358 | length=4; 359 | break; 360 | case 'm': 361 | //strcpy(temp,"--"); 362 | code=B11; 363 | length=2; 364 | break; 365 | case 'n': 366 | //strcpy(temp,"-."); 367 | code=B10; 368 | length=2; 369 | break; 370 | case 'o': 371 | //strcpy(temp,"---"); 372 | code=B111; 373 | length=3; 374 | break; 375 | case 'p': 376 | //strcpy(temp,".--."); 377 | code=B0110; 378 | length=4; 379 | break; 380 | case 'q': 381 | //strcpy(temp,"--.-"); 382 | code=B1101; 383 | length=4; 384 | break; 385 | case 'r': 386 | //strcpy(temp,".-."); 387 | code=B010; 388 | length=3; 389 | break; 390 | case 's': 391 | //strcpy(temp,"..."); 392 | code=B000; 393 | length=3; 394 | break; 395 | case 't': 396 | //strcpy(temp,"-"); 397 | code=B1; 398 | length=1; 399 | break; 400 | case 'u': 401 | //strcpy(temp,"..-"); 402 | code=B001; 403 | length=3; 404 | break; 405 | case 'v': 406 | //strcpy(temp,"...-"); 407 | code=B0001; 408 | length=4; 409 | break; 410 | case 'w': 411 | //strcpy(temp,".--"); 412 | code=B011; 413 | length=3; 414 | break; 415 | case 'x': 416 | //strcpy(temp,"-..-"); 417 | code=B1001; 418 | length=4; 419 | break; 420 | case 'y': 421 | //strcpy(temp,"-.--"); 422 | code=B1011; 423 | length=4; 424 | break; 425 | case 'z': 426 | //strcpy(temp,"--.."); 427 | code=B1100; 428 | length=4; 429 | break; 430 | case '0': 431 | //strcpy(temp,"-----"); 432 | code=B11111; 433 | length=5; 434 | break; 435 | case '1': 436 | //strcpy(temp,".----"); 437 | code=B01111; 438 | length=5; 439 | break; 440 | case '2': 441 | //strcpy(temp,"..---"); 442 | code=B00111; 443 | length=5; 444 | break; 445 | case '3': 446 | //strcpy(temp,"...--"); 447 | code=B00011; 448 | length=5; 449 | break; 450 | case '4': 451 | //strcpy(temp,"....-"); 452 | code=B00001; 453 | length=5; 454 | break; 455 | case '5': 456 | //strcpy(temp,"....."); 457 | code=B00000; 458 | length=5; 459 | break; 460 | case '6': 461 | //strcpy(temp,"-...."); 462 | code=B10000; 463 | length=5; 464 | break; 465 | case '7': 466 | //strcpy(temp,"--..."); 467 | code=B11000; 468 | length=5; 469 | break; 470 | case '8': 471 | //strcpy(temp,"---.."); 472 | code=B11100; 473 | length=5; 474 | break; 475 | case '9': 476 | //strcpy(temp,"----."); 477 | code=B11110; 478 | length=5; 479 | break; 480 | case ' ': 481 | //strcpy(temp,""); 482 | code=B0; 483 | length=0; 484 | delay(7*ditLen); 485 | break; 486 | case '.': 487 | //strcpy(temp,".-.-.-"); 488 | code=B010101; 489 | length=6; 490 | break; 491 | case '/': 492 | //strcpy(temp,"-..-."); 493 | code=B10010; 494 | length=5; 495 | break; 496 | case '-': 497 | //strcpy(temp,"-....-"); 498 | code=B100001; 499 | length=6; 500 | break; 501 | case '?': 502 | //strcpy(temp,"..--.."); 503 | code=B001100; 504 | length=6; 505 | break; 506 | case '@': //debug symbol 507 | code=analogRead(radioA.voxPin)/4; 508 | length=8; 509 | break; 510 | case '%': //debug symbol 511 | code=analogRead(radioB.voxPin)/4; 512 | length=8; 513 | break; 514 | default: 515 | //strcpy(temp,""); 516 | code=B0; 517 | length=0; 518 | break; 519 | } 520 | 521 | while(length > 0) 522 | { 523 | //determine if it's a dit or a dot 524 | if(code & bitMask(length)) 525 | { 526 | //1 is a dit 527 | tone(codePin,tonePitch); 528 | delay(3*ditLen); 529 | noTone(codePin); 530 | delay(ditLen); 531 | } 532 | else 533 | { 534 | //0 is a dot 535 | tone(codePin,tonePitch); 536 | delay(ditLen); 537 | noTone(codePin); 538 | delay(ditLen); 539 | } 540 | length--; 541 | } 542 | delay(ditLen); 543 | } 544 | } 545 | 546 | //generates a "bit mask" for getting nth bit from int 547 | int bitMask(int bitNumber) 548 | { 549 | int value=1; 550 | for(int x=1; x < bitNumber; x++) 551 | value*=2; 552 | return value; 553 | } 554 | 555 | //gets voltage from analog input pin 556 | float getPowerVoltage(int pin) 557 | { 558 | // R1 = 2200 (Vin to midpoint) 559 | // R2 = 1000 (midpoint to gnd) 560 | // put 5.1v protectioin zener in parallel for R2 to protect arduino input against overvolt 561 | // formula: 562 | // ( value/1023 * (arduino vcc) ) / ( R2 / (R2 + R1) ) + (Vin diode drop) 563 | //return ((analogRead(pin)/1023.0)*5.0) / (1000.0 / (2200.0 + 1000.0)) + 0.76 ; 564 | return (analogRead(pin)*0.0156402737047898) + 0.76; //simplified 565 | } 566 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 mmiller7 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino_vox_repeater 2 | 3 | The arduino sketch begins with some comments showing sample data that I used for some of my testing. The pin numbers will vary depending on your Arduino or ATTINY and how you wired up the circuit to which pins. 4 | 5 | The first section you need to modify is the #define statments starting with the CALLSIGN field. 6 | The other section you need to modify is the first two sections in setup() the lines for radioA and radioB settings. 7 | --------------------------------------------------------------------------------