Craig T
Published © GPL3+

Motorola Pager given new life with Bluetooth BLE

This is an old school 90s Motorola Advisor pager receiving message notifications from an Android phone.

IntermediateFull instructions provided5 hours87
Motorola Pager given new life with Bluetooth BLE

Things used in this project

Hardware components

Seeed Studio Seeed XIAO ESP32C6
×1
Arduino Pro Mini 328 - 3.3V/8MHz
SparkFun Arduino Pro Mini 328 - 3.3V/8MHz
×1
USB to TTL converter
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

Custom Battery Holder

Should work in just about every 3D software

Sketchfab still processing.

Schematics

Connections

Code

ble_pager.ino

Arduino
This is the code for the XAIO ESP32C6 module. This will connect via the hardware serial to the serial interface of the Arduino Pro Mini.
/*
   MIT License

  Copyright (c) 2023 Felix Biego

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.

  This is a stripped version of Fbiegos watch software, this will go along with POCSAG software to allow a
  smart phone to send notifications to an ESP32 with BLE which will spit out POCSAG to a Motorola
  Advisor pager. 

  */

#include <ChronosESP32.h>
#include <HardwareSerial.h>

HardwareSerial mySerial(0);
ChronosESP32 watch("Advisor"); // set the bluetooth name

void connectionCallback(bool state)
{
  Serial.print("Phone: ");
  Serial.println(state ? "Connected" : "Disconnected");
}

void notificationCallback(Notification notification)
{
   if (notification.title == "Device pairing") {

  } else {
  mySerial.print(notification.title);
  mySerial.print(" - ");
  mySerial.println(notification.message);
  }
}

void ringerCallback(String caller, bool state)
{
  if (state)
  {
    mySerial.print("Call-");
    mySerial.println(caller);
  }
  else
  {
    //Serial.println("Ringer dismissed");
  }
}

void configCallback(Config config, uint32_t a, uint32_t b)
{
  switch (config)
  {
  case CF_TIME:

    break;
  case CF_RTW:

    break;
  case CF_RST:

    break;
  case CF_FIND:

    break;
  case CF_FONT:

    break;
  case CF_ALARM:

    break;
  case CF_QUIET:

    break;
  case CF_SLEEP:

    break;
  case CF_SED:

    break;
  case CF_WATER:

    break;
  case CF_USER:

    break;
  case CF_HOURLY:

    break;
  case CF_HR24:

    break;
  case CF_CAMERA:

    break;
  case CF_LANG:

    break;
  case CF_PBAT:

    break;
  case CF_APP:

    break;
  case CF_QR:
    // qr links
    if (a == 0){

    }
    if (a == 1)
    {

    }
    break;
  case CF_WEATHER:

    if (a)
    {
      // if a == 1, high & low temperature values might not yet be updated
      if (a == 2)
      {
        int n = watch.getWeatherCount();
        String updateTime = watch.getWeatherTime();

        for (int i = 0; i < n; i++)
        {
          // iterate through weather forecast, index 0 is today, 1 tomorrow...etc
          Weather w = watch.getWeatherAt(i);

          if (i == 0)
          {

          }
        }
      }
    }
    if (b)
    {
      //Serial.print("City name: ");
      String city = watch.getWeatherCity(); //
      //Serial.print(city);
    }
    //Serial.println();
    break;
  case CF_CONTACT:
    if (a == 0){

    }
    if (a == 1){
      //Serial.println("Received all contacts");
      int n = uint8_t(b); // contacts size -> watch.getContactCount();
      int s = uint8_t(b >> 8); // sos contact index -> watch.getSOSContactIndex();
      for (int i = 0; i < n; i++)
      {
        Contact cn = watch.getContact(i);

      }
    }
    break;
  }
}

void dataCallback(uint8_t *data, int length)
{
  //Serial.println("Received Data");
  for (int i = 0; i < length; i++)
  {
    //Serial.printf("%02X ", data[i]);
  }
  //Serial.println();
}

void setup()
{
  Serial.begin(9600);
  mySerial.begin(9600);

  // set the callbacks before calling begin funtion
  watch.setConnectionCallback(connectionCallback);
  watch.setNotificationCallback(notificationCallback);
  watch.setRingerCallback(ringerCallback);
  watch.setConfigurationCallback(configCallback);
  watch.setDataCallback(dataCallback);

  watch.begin(); // initializes the BLE

  watch.setBattery(80); // set the battery level, will be synced to the app

  watch.set24Hour(true); // the 24 hour mode will be overwritten when the command is received from the app
  // this modifies the return of the functions below
  watch.getAmPmC(true); // 12 hour mode true->(am/pm), false->(AM/PM), if 24 hour mode returns empty string ("")
  watch.getHourC();     // (0-12), (0-23)
  watch.getHourZ();     // zero padded hour (00-12), (00-23)
  watch.is24Hour();     // resturns whether in 24 hour mode
  // watch.setNotifyBattery(false); // whether to enable or disable receiving phone battery status (enabled by default)
}

void loop()
{
  watch.loop(); // handles internal routine functions

  String time = watch.getHourC() + watch.getTime(":%M ") + watch.getAmPmC();
  //Serial.println(time);
  delay(500);

}

AdvisorRadio.ino

Arduino
This is the code that needs to go onto the Arduino Pro Mini 3.3v 8mhz. For this code to run properly, this is the only Arduino capable at this point of creating a proper POCSAG message for the pager.
// POCSAG pieces of this code is based on rpitx by F5OEO (https://github.com/F5OEO/rpitx)
// so here's its license:
/*MIT License
Copyright (c) 2016 Galen Alderson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Fork and modification for rpitx (c)(F5OEO 2018)
** 11.09.2019 : Added Numeric Pager support by cuddlycheetah (github.com/cuddlycheetah)
** 14.10.2019 : Added Repeating Transmission + Single Preamble Mode
**
** fall 2021  : Modifications to run on an Arduino Pro Mini by Pena
** fall 2025  : Stripped a bit and modified to run as user input
** This was modified to run on the Arduino Pro Mini, the missing information on that is, this will only run
** on the 3.3v 8mhz board at this point. Attempting to run on anything with a higher clock rate yields bad POCSAG encoding (Timing issue)
** This version waits for a message from serial input, then creates POCSAG out to the pager using the static capcode set in the software. Works on the
** Motorola Advisor, not sure about others, but if the radio is removable, good chance this will work with just about
** any POCSAG pager. Set to inverted and 512 bps, just change the CAP Code to yours. Have not attempted this with anything faster than 512 bps 
*/

#include <time.h>
// Settings and constants for POCSAG
#define sleeptime 1500
#define SYNC 0x7CD215D8
#define IDLE 0x7A89C197
#define FRAME_SIZE 2
#define BATCH_SIZE 16
#define PREAMBLE_LENGTH 576
#define FLAG_ADDRESS 0x000000
#define FLAG_MESSAGE 0x100000
#define FLAG_TEXT_DATA 0x3
#define TEXT_BITS_PER_WORD 20
#define TEXT_BITS_PER_CHAR 7
#define CRC_BITS 10

long mypocsag = 1992236; // <- Set your capcode here

// Functions borrowed from rpitx start here:
uint32_t crc(uint32_t inputMsg) {
  uint32_t denominator = 0b11101101001;  // Need to set this value first before shifting
  denominator = denominator << 20;       // because denominator would now be zero on one line
  uint32_t msg = inputMsg << CRC_BITS;
  for (int column = 0; column <= 20; column++) {
    int msgBit = (msg >> (30 - column)) & 1;
    if (msgBit != 0) msg ^= denominator;
    denominator >>= 1;
  }
  return msg & 0x3FF;
}

uint32_t parity(uint32_t x) {
  uint32_t p = 0;
  for (int i = 0; i < 32; i++) {
    p ^= (x & 1);
    x >>= 1;
  }
  return p;
}

uint32_t encodeCodeword(uint32_t msg) {
  uint32_t fullCRC = (msg << CRC_BITS) | crc(msg);
  uint32_t p = parity(fullCRC);
  return (fullCRC << 1) | p;
}

uint32_t encodeASCII(uint32_t initial_offset, char *str, uint32_t *out) {
  uint32_t numWordsWritten = 0;
  uint32_t currentWord = 0;
  uint32_t currentNumBits = 0;
  uint32_t wordPosition = initial_offset;
  while (*str != 0) {
    unsigned char c = *str;
    str++;
    for (int i = 0; i < TEXT_BITS_PER_CHAR; i++) {
      currentWord <<= 1;
      currentWord |= (c >> i) & 1;
      currentNumBits++;
      if (currentNumBits == TEXT_BITS_PER_WORD) {
        *out = encodeCodeword(currentWord | FLAG_MESSAGE);
        out++;
        currentWord = 0;
        currentNumBits = 0;
        numWordsWritten++;
        wordPosition++;
        if (wordPosition == BATCH_SIZE) {
          *out = SYNC;
          out++;
          numWordsWritten++;
          wordPosition = 0;
        }
      }
    }
  }
  if (currentNumBits > 0)
  {
    currentWord <<= 20 - currentNumBits;
    *out = encodeCodeword(currentWord | FLAG_MESSAGE);
    out++;
    numWordsWritten++;
    wordPosition++;
    if (wordPosition == BATCH_SIZE) {
      *out = SYNC;
      out++;
      numWordsWritten++;
      wordPosition = 0;
    }
  }
  return numWordsWritten;
}

int addressOffset(long address) {
  return (address & 0x7) * FRAME_SIZE;
}

void encodeTransmission(int repeatIndex, long address, int fb, char *message, uint32_t *out) {
  if (repeatIndex == 0)
    for (int i = 0; i < PREAMBLE_LENGTH / 32; i++) {
      *out = 0xAAAAAAAA;
      out++;
    }
  uint32_t *start = out;
  *out = SYNC;
  out++;
  int prefixLength = addressOffset(address);
  for (int i = 0; i < prefixLength; i++) {
    *out = IDLE;
    out++;
  }
  *out = encodeCodeword(((address >> 3) << 2) | fb);
  out++;
  out += encodeASCII(addressOffset(address) + 1, message, out);
  *out = IDLE;
  out++;
  size_t written = out - start;
  size_t padding = (BATCH_SIZE + 1) - written % (BATCH_SIZE + 1);
  for (size_t i = 0; i < padding; i++) {
    *out = IDLE;
    out++;
  }
}

size_t textMessageLength(int repeatIndex, long address, int numChars) {
  size_t numWords = 0;
  numWords += addressOffset(address);
  numWords++;
  numWords += (numChars * TEXT_BITS_PER_CHAR + (TEXT_BITS_PER_WORD - 1)) / TEXT_BITS_PER_WORD;
  numWords++;
  numWords += BATCH_SIZE - (numWords % BATCH_SIZE);
  numWords += numWords / BATCH_SIZE;
  if (repeatIndex == 0) numWords += PREAMBLE_LENGTH / 32;
  return numWords;
}
// Functions borrowed from rpitx end here.
//#define dapin 3
#define pogsacpin 7 
          // 7 = POCSAG transmission pin.
int SetRate;
int pindelay;
int SetFunctionBits;
bool SetInverted;

void setup() {
  // Initializing serial port and throwing something for debug purposes, *IF* serial is connected.

  if (Serial) Serial.begin(9600);

  // Setting pins
  pinMode(pogsacpin, OUTPUT);
  pinMode(3, OUTPUT);
  digitalWrite(pogsacpin, HIGH);
  //digitalWrite(dapin, HIGH);

  // POGSAC settings
  SetRate = 512;
  pindelay = (1000000 / SetRate) * 0.991;  // YMMV, mine was a bit too slow w/o multiplier
  SetFunctionBits = 2;
  SetInverted = true;
}

void wakeUp() {
  asm volatile ("jmp 0x7800");
}

void loop() {

  Serial.println("=== Local Pager Message Sender ===");
  Serial.println("Just type in your message, hit enter");
  Serial.print("Enter message: ");
  
  // Wait for user input
  String userInput = "";

  while (true) {
    if (Serial.available()) {
      char c = Serial.read();
      
      if (c == '\n' || c == '\r') {
        // Enter pressed - process command if not empty
        if (userInput.length() > 0) {
          Serial.println(); // New line after input
          break;
        }
      } else if (c >= 32 && c <= 126) {
        // Printable character
        userInput += c;
        Serial.print(c); // Echo character
      } else if (c == 8 || c == 127) {
        // Backspace
        if (userInput.length() > 0) {
          userInput.remove(userInput.length() - 1);
          Serial.print("\b \b"); // Erase character on screen
        }
      }
    }

    delay(10);
  }
  
  userInput.trim();
  String messagetx = "";
  messagetx = userInput;

 if (messagetx.length() == 0) {
    Serial.println("Error: Empty message. Please try again.");
    delay(2000);
    return;
  }
  
  if (messagetx.length() > 160) {
    Serial.println("Warning: Message truncated to 160 characters");
    messagetx = messagetx.substring(0, 160);
  }

  char bufferi[180];
  int readbytes = 0;
    String s = "123456:" + messagetx + "_";
    readbytes = s.length();
    s.toCharArray(bufferi, readbytes);

  int colonIndex = 0;
  char addr[8];
  for(int i = 0; i <= sizeof(addr); i++) { addr[i] = (char)0; }
  char message[160];
  for(int i = 0; i <= sizeof(message); i++) { message[i] = (char)0; }
  for(int i = 0; i <= readbytes; i++) {
    if(colonIndex>0) message[i - colonIndex - 1] = bufferi[i];
    if(bufferi[i] == ':') colonIndex = i;
    if((colonIndex==0) && (i < 8)) addr[i-1] = bufferi[i];
  }
 
  // Zero rest of the message
  for(int i = readbytes - 1; i <= sizeof(message); i++) { message[i] = (char)0; }
  // Reset buffer back to zero
  for(int i = 0; i < sizeof(bufferi); ++i) bufferi[i] = (char)0;

  srand(pindelay);

  size_t completeLength = 0;
  uint32_t *completeTransmission = (uint32_t *)malloc(sizeof(uint32_t) * 0);
  size_t messageLength = textMessageLength(0, mypocsag, strlen(message));
  uint32_t *transmission = (uint32_t *)malloc(sizeof(uint32_t) * messageLength);
  encodeTransmission(0, mypocsag, SetFunctionBits, message, transmission);
  size_t beforeLength = completeLength + 0;
  completeLength += messageLength;
  completeTransmission = (uint32_t *)realloc(completeTransmission, sizeof(uint32_t) * completeLength);
  for (size_t byteI = 0; byteI < messageLength; byteI++) {
    completeTransmission[beforeLength + byteI] = transmission[byteI];
  }

  // Now we have raw POCSAG stuff in a variable, need to make the pin pulse them to the pager
  static uint32_t oldtime = micros();
  for (int i = 0; i < completeLength; i++) {
    if (!SetInverted) completeTransmission[i] = ~completeTransmission[i];
    for (int j = 31; j >= 0; j--) {
      int b = (completeTransmission[i] >> j) & 0x1;
      if(b==1) { digitalWrite(pogsacpin, LOW); } else { digitalWrite(pogsacpin, HIGH); }
      while (micros() - oldtime < pindelay) {}
      oldtime = micros();
    }
   }
 
  digitalWrite(pogsacpin, HIGH);
  if (Serial) Serial.println("POGSAC done, reset...");
  delay(50);
  asm volatile ("jmp 0x7800");
 
 }

Chronos-ESP32 Arduino Library

This is the back end code for ble_pager.ino, this connects from the ESP32C6 via blue tooth to an android phone.

Credits

Craig T
1 project • 0 followers

Comments