De 'Nostalfoon'

Van een oom, die als vrijwilliger werkt bij een tehuis voor dementerenden, kwam de vraag of ik een oude kiesschijftelefoon kon ombouwen tot MP3 speler met nostalgische liedjes en radiofragmenten. Het weerzien van, en de herinneringen aan, de oude getrouwe PTT telefoon met draaischijf, én het beluisteren van oude herinnerde fragmenten, blijkt vaak een feest der herkenning.

Inmiddels bouwde ik al een stel 'Nostalfoons' en zocht uit hoe, behalve het vertrouwde T65-toestel van de Nederlandse PTT, ook een hedendaags retro-model kiesschijftelefoon (de alom verkrijgbare GPO746 remake) een tweede leven als muziekspeler kan krijgen.

Video : toestemming voor cookies nodig
Instellingen

Het idee

Het idee een oude kiesschijftelefoon voor moderne projecten te gebruiken is niet nieuw. Toen ik ging zoeken, bleken er al best wat mensen bezig te zijn geweest met het uitlezen van de pulsen van de kiesschijf van dergelijke klassieke telefoontoestellen. Het basisidee van het tehuis bleek de Nederlandse 'Wonderfoon'. Maar tijdens het experimenteel bouwen van dit ontwerp (opgebouwd rond een complete Raspberry Pi met Linux OS) kwam dit op mij, voor een simpele keuze uit slechts tien MP3-geluidsfragmenten, onnodig complex over. Inmiddels heeft de 'uitvinder' dit ook ingezien en biedt inmiddels een 'centrale'-module waarmee álle telefoons kunnen worden gebruikt om MP3-fragmenten te kunnen beluisteren. Ook is hiermee inmiddels gelukkig behoorlijk wat functionaliteit toegevoegd. Een andere aanbieder biedt setjes om zelf een T65 om te bouwen. Zij bieden ook een prima beschrijving van de ombouw, maar maken echter gebruik van een Wemos D1 Mini of ATTiny85 in plaats van de door mij gebruikte Arduino Nano.

Kortom, veel is zó al te koop, maar zelf iets knutselen is natuurlijk het leukst. Wellicht kan ik anderen nog inspireren of helpen met mijn notities hier. Bovendien biedt zelfbouw het voordeel dat je het precies zó kunt maken als je wilt. En er waren eigenlijk gaandeweg al behoorlijk wat 'eigen' wensen ingeslopen. Liefst wilde ik, als ik toch een micro SD-kaartje zou plaatsen, minimaal honderd verschillende fragmenten. Het leek me mooi ook nostalgische radiofragmenten (o.a. uit de oorlogstijd) te kunnen opnemen, wilde een tweekleurige controle LED, een échte goede kiestoon (mooie 'warme' sinus met de originele combinatie van 150 Hz en 450 Hz) en een toepassing, bijvoorbeeld een random-functie, voor de 'flash'/'doorverbind' knop op sommige T65's. Enzovoort 😉

De T65 kiesschijf telefoon

T65_telefoon_300x234
Het telefoontoestel 'T65' werd vanaf oktober 1965 tot en met december 1987 door de Nederlandse PTT aan abonnees geleverd. In de naam staat de 'T' voor 'Tafeltoestel' en '65' voor het jaar van introductie. Voor veel mensen was dit toestel de eerste telefoon. De standaardkleur voor dit huurtoestel was grijs met crème accenten. Tegen meerprijs was dit toestel later ook in diverse gekleurde varianten leverbaar.

Pas op 1 januari 1988 kwam de PTT met een opvolger voor de T65. Het nieuwe standaard huurtoestel heette, niet verrassend, 'T88' en was instelbaar op puls- of toonkiezen. In de jaren daarna zullen tienduizenden ingeruilde T65 toestellen door de PTT zijn verschroot. Toch zijn er nog erg veel T65 telefoons bewaard gebleven. Op online verkoopsites of in kringloopwinkels kom je ze nog geregeld tegen. Ook is er gelukkig online nog veel informatie te vinden over deze markante telefoon, bijvoorbeeld op Wikipedia.

De techniek

Na wat spelen kwam ik uit op een Arduino Nano en Mini-MP3-player module. Deze player module kan direct WAV- en MP3-audio afspelen. Na toevoeging van wat kleinmateriaal (LEDje, speakertje, gaatjesprint, etc) en een micro SD-kaart is een en ander relatief goedkoop en eenvoudig in elkaar te solderen.

Voeg een netadaptor toe voor de voedingsspanning en de gehele gewenste functionaliteit is verder in de Arduino te programmeren. Natuurlijk valt of staat het succes van het project met de op de micro SD-kaart ter beschikking staande MP3-/WAV-fragmenten en liedjes.

IMG_0483

Geluid op de micro SD-kaart

De Arduino-Sketch rekent op 10 subdirectories met ieder 10 WAV- (of MP3-)files er in op de micro SD-kaart. Het eerste, in de root van de disk, gevonden file wordt gebruikt als kiestoon. Alle adressering van fragmenten op de SD-kaart, is op alfabetisch op de naamgeving van de subdirectories en bestanden. Wie hier een reproduceerbare volgorde in wil, doet er dus goed aan de subdirectories namen als '01', '02', '03', etc. te geven en voor de bestanden in elke subdirectory bijvoorbeeld '001.wav', '002.wav', etc. te kiezen. Zelf koos ik voor WAV mono 32kHz 16 bits audiobestanden en maakte een linux-scriptje om na overkopieëren van alle fragmenten de bestandsnamen op de SD-kaart volgens dit stramien numeriek te maken. Gooi ook eventuele onzichtbare (controle-)bestanden van de SD-kaart af zodat deze de volgorde niet kunnen verstoren. Voor de kiestoon wordt bijvoorbeeld simpelweg het eerst gevonden file in de FAT (File Allocation Table) gebruikt.

Het is me helaas niet toegestaan alle WAV-files hier te publiceren, maar de lijst met audio-fragmenten die ik verzamelde is hier te downloaden als PDF file.

Hiernaast het Linux/Bash-script dat ik maakte om op de SD-kaart tot het laatste moment te kunnen blijven werken met leesbare menselijke bestandsnamen. Eventueel activeert  u ook nog het stukje code dat de directorynamen aanpast. Ik koos er echter voor deze al gewoon '01', '02', etc. te noemen in mijn hoofdbibliotheek met WAV-bestanden.

#!/bin/bash

# Renames dir structure and filenames to numbers. Keeps the file extention...
# AvD nov 2019
#
#subname=1

# First rename the subdirs....
#for subdir in $(find . -maxdepth 1 -type d|sort)
#do
#  if [[ "$subdir" == "." ]]; then
#    continue
#  fi
#  mv $subdir 0$subname
#  echo "$subdir was moved to 0$subname"
#  ((subname=subname+1))
#done

# Now do the individual files...
for subdir in $(find . -maxdepth 1 -type d|sort)
do
  if [[ "$subdir" == "." ]]; then
    continue
  fi
  filecntr=1;
  for wavfile in $subdir/*.wav ; do
    if [[ "$wavfile" == "." ]]; then
      continue
    fi
    if [[ "$wavfile" == ".." ]]; then
      continue
    fi
    safename=${wavfile// /_}
    mv "$wavfile" "${wavfile// /_}"
    if [ $filecntr -lt 10 ]; then
      mv $safename $subdir/00${filecntr}.wav
    else
      mv $safename $subdir/0${filecntr}.wav    
    fi
    ((filecntr=filecntr+1))
  done
done
find . -type f -name '.[_|DS]*' -exec rm {} \;
echo "All done..."

Een puls-kiesschijf uitlezen

Voor het afspelen van de muziekfragmenten wordt een MP3/WAV-playermodule gebruikt die door de Arduino Nano wordt aangestuurd. Een klein speakertje met een impedantie van 8Ω vervangt het originele hoogohmige element in de hoorn om de muziek hoorbaar te maken. De Arduino moet kunnen detecteren dat de hoorn wordt opgenomen (de groene LED gaat knipperen) en daarna het met de kiesschijf 'gekozen' nummer gebruiken om een muziekfragment te laten spelen. Allereerst zal dus een ouderwetse kiesschijf moeten kunnen worden 'gelezen'.

kiesschijf_detail
Uit de originele kiesschijf van een T65 toestel komen drie gekleurde draden. Bij het gebruik van de kiesschijf worden de rode en gele draad in een pulsen-patroon verbonden met de blauwe draad. Deze blauwe draad is hierbij aan de 'ground' (massa) verbonden. Om de pulsen uit te lezen zijn de gele en rode draad verbonden aan respectievelijk de D5 en D6 ingang van de Arduino Nano. Deze ingangen hebben softwarematig een PULLUP-weerstand toegekend gekregen zodat de signalen in rust HIGH zijn. Zodra de kiesschijf wordt gedraaid worden beide draden LOW.

IMG_0473
Binnen de lange puls van de gele draad, die pas weer HIGH eindigt als de kiesschijf geheel op eigen kracht is teruggedraaid, pulseert de rode draad tijdens dit vanzelf terugdraaien van de kiesschijf, een reeks kortere HIGH pulsen. Dit aantal pulsen komt overeen met het gedraaide nummer. In de afbeelding hierboven is dit zichtbaar. Er werd een '0' (tien pulsen) gedraaid. Deze pulsen kunnen in een Arduino Sketch worden geteld waarbij de lange puls van de gele draad als telperiode voor dit telproces kan dienen. Op deze wijze kan met met behulp van een aantal timers een dergelijke kiesschijf dus nauwkeurig worden uitgelezen.

Nostalphone_T65_bb

De Arduino Sketch voor de T65

Hiernaast vind je de Arduino Sketch die ik maakte voor de T65 telefoon. Om de WAV/MP3-player over SoftwareSerial te controleren gebruik ik de DFRobotDFPlayerMini library.

In het front van de T65 boorde ik een gaatje voor een 2-kleuren (rood/groen) LED. Als de telefoon in bedrijf is brandt de groene LED, die gaat knipperen als de hoorn wordt opgenomen. Nadat een nummer gedraaid is, brandt de LED rood en klinkt het gekozen geluidsfragment.

Veel T65 toestellen hebben een extra 'flash'/doorverbind-knop onder de kiesschijf. Ik heb het nu zo geprogrammeerd dat als deze wordt ingedrukt, wordt een willekeurig nummer door de speler ten gehore gebracht.

// Whizzbizz.com - Arnoud van Delden - November 2019 (for 'uncle Thijs')
//
// Nostalphone Sketch for the PTT T65 rotary dial phone. Waits for hook switch and then reads the number from the rotary dial
// The 'flash'-button on the T65 is used to make a direct random selection from all samples, otherwise the first (or only) number
// determines the subdir/bank to be used (each bank has 10 WAVs) and the last number dialed the WAV within this subdirectory/bank.
// Samples are played through DFPlayer (Mini MP3 Player) controlled over SoftwareSerial with RX=10 and TX=11
//
// Expects a SD-card with the correct directory structure of ten banks/subdirs of ten WAVs each
// The bank/subdir 0/ contains the sounds that are played when only one number was dialed
// Otherwise first number is the bank/subdir, last number dialed the sound that is to be played...
// First WAV on SD is played as dial tone, so provide a WAV-file starting with '00' (e.g. '00 Kiestoon350Hz.wav') in the root of the SD card
//

#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

#define SD_ChipSelectPin 10
#define BlinkTime 10000
#define rand_button 4 // Random button (high active)
#define input_dial  5 // Rotary dial active (yellow)
#define input_pulse 6 // Rotary dial pulses (green)
#define input_hook  7 // Pin hook switch connected to (purple)
#define led_green   8 // Green LED: lit on power on, flashes when dialing
#define led_red     9 // Red LED: lit when music is played

unsigned long debounce_time_hook  = 0; // Debounce time for this button/pin...
unsigned long debounce_time_dial  = 0; // Debounce time for this button/pin...
unsigned long debounce_time_pulse = 0; // Debounce time for this button/pin...
unsigned long debounce_time_rand  = 0; // Debounce time for this button/pin...
unsigned long dialing_timeout = 0;     // To keep track of dialing time out...
unsigned long debounceDelay = 50;      // Global debounce time...

bool logging = false; // Serial logging during development...
bool hook_up = false;
bool dialing = false;
bool process_number = false;
bool number_detected = false;

int hook_state;
int pulse_state;
int rand_state;
int last_hook_state = LOW;
int last_dialing_state = HIGH;
int last_io_state_pulse = HIGH;
int last_rand_button_state = HIGH;
int last_number_dialed = 0;
int nrs_dialed = 0;
int pulse_count = 0;
int blink_count = 0; // For the blinking LED...

int rotary_dialing;
int io_state_pulse;
int hook_switch_state;
int rand_button_state;

int random_bank;
int random_song;
int bank_nr = 0;

SoftwareSerial mySoftwareSerial(10, 11); // RX, TX
DFRobotDFPlayerMini myDFPlayer;
void printDetail(uint8_t type, int value);

void setup() {
  mySoftwareSerial.begin(9600);
  Serial.begin(38400);
      
  pinMode(input_hook, INPUT_PULLUP);
  pinMode(input_pulse, INPUT_PULLUP);
  pinMode(input_dial, INPUT_PULLUP);
  pinMode(led_red, OUTPUT);
  pinMode(led_green, OUTPUT);

  logSerial("Nostalphone v1.3");
  logSerial("Initializing DFPlayer...");
  if (!myDFPlayer.begin(mySoftwareSerial)) {  //Use softwareSerial to communicate with mp3.
    logSerial("Unable to begin:");
    logSerial("1.Please recheck the connection!");
    logSerial("2.Please insert the SD card!");
    while(true);
  }
  logSerial("DFPlayer Mini online.");

  myDFPlayer.setTimeOut(500); //Set serial communictaion time out 500ms
  
  //----Set volume----
  myDFPlayer.volume(15);  //Set volume value (0~30).
  myDFPlayer.volumeUp(); //Volume Up
  myDFPlayer.volumeDown(); //Volume Down
  myDFPlayer.EQ(DFPLAYER_EQ_NORMAL);
  myDFPlayer.outputDevice(DFPLAYER_DEVICE_SD);

  if (0) { // Debug diagnostics only: dumps nr of files in each subfolder of the SD card
    Serial.print("1 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(1));
    Serial.print("2 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(2));
    Serial.print("3 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(3));
    Serial.print("4 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(4));
    Serial.print("5 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(5));
    Serial.print("6 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(6));
    Serial.print("7 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(7));
    Serial.print("8 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(8));
    Serial.print("9 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(9));
    Serial.print("10 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(10));          
  }

  digitalWrite(led_green, LOW);  // turn the green LED on on power on...
  digitalWrite(led_red, HIGH);   // turn the red LED off (HIGH=off)
  
  logSerial("Setup done..."); 
}

void logSerial(String message) {
  // Handled overhere so logging to serial can easily be switched off...
  if (logging) Serial.println(message);
  return;
}

void loop() {
  // Read hook state...
  hook_switch_state = digitalRead(input_hook);  
  if (hook_switch_state != last_hook_state) {
    debounce_time_hook = millis();
  }
  if ((millis() - debounce_time_hook) > debounceDelay) {
    if (hook_switch_state != hook_state) {
      hook_state = hook_switch_state;
      if (hook_state == HIGH) {
        logSerial("Receiver was picked up. Please dial a number...");
        hook_up = true;
        bank_nr = -1;
        nrs_dialed = 0;
        // Output dial tone...
        myDFPlayer.play(1);
      } else {
        logSerial("Receiver is put back. Thanx for using the Nostalphone!");
        hook_up = false;
        myDFPlayer.stop(); // End dial tone...
        digitalWrite(led_green, LOW);  // turn the green LED on on power on...
        digitalWrite(led_red, HIGH);   // turn the red LED off (HIGH=off)
      }
    }
  }
  last_hook_state = hook_switch_state;
  
  if (hook_up) {
    // Read rotary dial state...
    rotary_dialing = digitalRead(input_dial);
    if (rotary_dialing != last_dialing_state) {
      debounce_time_dial = millis();
    }
    
    if ((millis() - debounce_time_dial) > debounceDelay) {
      // Rotary dial T65 dial logic...
      if (!dialing && rotary_dialing==LOW) { // Start using the rotary dial...
        logSerial("Starting dialing phase...");  
        process_number = false;
        dialing = true;
        pulse_count = 0;
        nrs_dialed = 0;
        bank_nr = -1;
        myDFPlayer.stop(); // End dial tone...   
      }
      if (rotary_dialing==LOW) {
        number_detected = false;
        dialing_timeout = millis(); // Keep resetting timer during dialing...
        
        // Pulse count logic...
        io_state_pulse = digitalRead(input_pulse);
        if (io_state_pulse != last_io_state_pulse) {
          debounce_time_pulse = millis();
        }
        if ((millis() - debounce_time_pulse) > 20) {
          if (io_state_pulse != pulse_state) {
            pulse_state = io_state_pulse;
            if (io_state_pulse==HIGH) {
              pulse_count++; 
            }
          }
        }
        last_io_state_pulse = io_state_pulse;
      }

      if (dialing && rotary_dialing==HIGH) {
        // Rotary dial has walked back...
        number_detected = true;
        if (bank_nr<0) {
          bank_nr = pulse_count;
          logSerial("bank_nr: "+(String)bank_nr);
        }
      }

      if (number_detected==true && pulse_count>0) {
        last_number_dialed = pulse_count;
        nrs_dialed++;
        logSerial("last_number_dialed: "+(String)last_number_dialed);
        pulse_count = 0;
        number_detected = false;
      }
      
      if (dialing && ((millis() - dialing_timeout) > 1500)) {
        // Dial time out reached...
        logSerial("Dialing finished...");
        process_number = true;
        dialing = false;
      } 
    }
    last_dialing_state = rotary_dialing;

    if ((hook_up || dialing) && digitalRead(led_red)) {
      // Blink LED...
      if (blink_count > BlinkTime) {
        blink_count = 0;
        // Toggle blink...
        digitalWrite(led_green, !digitalRead(led_green));  // Toggle the green LED...
      }
      blink_count++;
    }

    // Read rand_button state...
    rand_button_state = digitalRead(rand_button);  
    if (rand_button_state != last_rand_button_state) {
      debounce_time_rand = millis();
    }
    if ((millis() - debounce_time_rand) > debounceDelay) {
      if (rand_button_state != rand_state) {
        rand_state = rand_button_state;
        if (rand_state == HIGH) {
          digitalWrite(led_green, HIGH);  // turn the green LED on on power off...
          digitalWrite(led_red, LOW);   // turn the red LED on (HIGH=off)
          process_number = false;
          random_bank = random(9)+1;
          random_song = random(9)+1;
          logSerial("Random bank "+(String)random_bank+" song "+(String)random_song);
          myDFPlayer.playFolder(random_bank, random_song); // Play a random song...
        }
      }
    }
    last_rand_button_state = rand_button_state;    

    if (process_number) {
      if (nrs_dialed==1) bank_nr = 1; // Only one number dialed: use default bank 1...

      // Play WAV....
      logSerial("bank_nr "+(String)bank_nr+" song "+(String)last_number_dialed);
      digitalWrite(led_green, HIGH); // Power off green LED...
      digitalWrite(led_red, LOW);    // Turn the red LED on (HIGH=off)

      process_number = false;
      if (myDFPlayer.available()) {
        printDetail(myDFPlayer.readType(), myDFPlayer.read()); //Print the detail message from DFPlayer to handle different errors and states.
      }  
      myDFPlayer.playFolder(bank_nr, last_number_dialed); // Play the song...
    }  
  } // End hook_up...  
}

void printDetail(uint8_t type, int value) {
  switch (type) {
    case TimeOut:
      logSerial("DFPlayer: Time Out!");
      break;
    case WrongStack:
      logSerial("DFPlayer: Stack Wrong!");
      break;
    case DFPlayerCardInserted:
      logSerial("DFPlayer: Card Inserted!");
      break;
    case DFPlayerCardRemoved:
      logSerial("DFPlayer: Card Removed!");
      break;
    case DFPlayerCardOnline:
      logSerial("DFPlayer: Card Online!");
      break;
    case DFPlayerUSBInserted:
      logSerial("DFPlayer: USB Inserted!");
      break;
    case DFPlayerUSBRemoved:
      logSerial("DFPlayer: USB Removed!");
      break;
    case DFPlayerPlayFinished:
      logSerial("DFPlayer: Number:"+(String)value+" Play Finished!");
      break;
    case DFPlayerError:
      switch (value) {
        case Busy:
          logSerial("DFPlayerError: Card not found");
          break;
        case Sleeping:
          logSerial("DFPlayerError: Sleeping");
          break;
        case SerialWrongStack:
          logSerial("DFPlayerError: Get Wrong Stack");
          break;
        case CheckSumNotMatch:
          logSerial("DFPlayerError: Check Sum Not Match");
          break;
        case FileIndexOut:
          logSerial("DFPlayerError: File Index Out of Bound");
          break;
        case FileMismatch:
          logSerial("DFPlayerError: Cannot Find File");
          break;
        case Advertise:
          logSerial("DFPlayerError: In Advertise");
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

De GPO746 telefoon

Nadat ik voor enkele kennissen T65's had omgebouwd, ontving ik een voor mij onbekende andere kiesschijftelefoon. Dit leek helemaal geen klassiek toestel te zijn, maar een ultramoderne remake van de een oude Britse 'General Post Office' (GPO) telefoon. Deze telefoon ziet er wel ouderwets uit, maar is in principe een ultra-moderne namaak vintage GPO746 telefoon. Een dergelijke telefoon is vrij eenvoudig te herkennen aan het feit dat men de modernere '*' en '#' van de druktoets-telefoons óók een plaatsje heeft gegeven op de kiesschijf. Wie zoekt op internet merkt al snel dat deze telefoons volop worden aangeboden. Mijn exemplaar bleek dan ook slechts enkele jaren oud. Vanzelfsprekend was het een leuke uitdaging hier ook een 'Nostalfoon' van te maken.

GPO_phone

De GPO746 kiesschijf

De puls-uitlezing die ik voor de T65 programmeerde, werkt hier niet omdat dit nep-retro model telefoon een compleet andere kiesschijf heeft. Bij het draaien beweegt een schijf met een opening boven een reeks van twaalf LDR's (lichtgevoelige weerstanden). De 'stop' waar je met de vinger tegenaan stoot bevat een schakelaar die twee LED's laat branden. De op dát moment beschenen LDR heeft door het opvallende licht een lagere weerstandswaarde waardoor dan het gedraaide nummer kan worden gedecodeerd.

IMG_0460IMG_0459IMG_0458

Een LDR-matrix uitlezen

GPO_dialmatrix
De LDR's van de kiesschijf van deze telefoon staan in principe in een matrix van drie vier rijen en drie kolommen geschakeld. Deze zo onstane scan-lijnen worden samen met de voedingslijnen voor de LED's én een uitlezing van de stop-/stoot-schakelaar op één lintkabel naar de hoofdprint van deze telefoon gevoerd. Na wat 'reverse engineering' kwam ik tot het aansluitschema hiernaast voor het uitlezen hiervan met de Arduino. Er wordt achtereenvolgens telkens voor één knooppunt een spanning van 5 volt aangelegd door de betreffende rij (A1 t/m A4) HIGH te maken, en de betreffende kolom (D2 t/m D4) LOW.

De weerstand van de LDR op het knooppunt bepaalt op dat moment de op de analoge ingangen te meten spanningswaarde op het midden van de spanningsdeler die de LDR maakt met de betreffende 1kΩ weerstand naar massa. Immers, als een LDR niet beschenen wordt is deze relatief hoog-ohmig. Valt er LED-licht op (indien dít nummer gedraaid werd) dan daalt de weerstand drastisch waardoor de te meten spanning stijgt. Dit kunnen we in de software detecteren. Het is omslachtig en vergt veel meer ingangen dan een puls-kiesschijf van de T65, maar het ging eigenlijk direct goed en blijkt behoorlijk betrouwbaar.

Nostalphone_GPO746_bb

De Arduino Sketch voor de GPO746

Het uitlezen van deze kiesschijf vergt een geheel andere aanpak. De Sketch hiernaast toont de code. De eind-schakelaar triggert een scan van het gehele matrix en daarmee uitlezing van alle twaalf spanningswaarden welke volgen uit de momentane LDR-waarden. De met LED-licht beschenen LDR (het gedraaide nummer) kan worden gedetecteerd als een hogere gemeten analoge spanning op het betreffende knooppunt tijdens de scan.

Aangezien de detectie bij dit model kiesschijf niet plaatsvindt met pulsen tijdens het terugdraaien van de kiesschijf, maar reeds bij het verste draaipunt van de schijf wordt bepaald, moest er wel een iets ruimere time-out worden ingesteld waarin de schijf kan terugdraaien en het eventuele vervolgnummer door de gebruiker kan worden gekozen. Anders zou het niet mogelijk zijn nummerreeksen ('telefoonnummers') te lezen. Ik programmeerde het namelijk, net als bij de T65 hierboven, zodanig dat het eerste gedraaide nummer de 'bank' (met elk tien fragmenten) bepaalt, en het laatst gekozen nummer het fragment binnen deze bank. Als men slechts één nummer draait, komt dit nummer uit bank 1.

Omdat deze telefoon geen extra knop onder de kiesschijf heeft, koos ik er voor de '#' en de '*' (en een simpelweg klikken aan de stop-eindknop zonder de kiesschijf te draaien) als willekeurig-fragment knop te laten functioneren.

// Whizzbizz.com - Arnoud van Delden - Easter/april 2021 (for 'aunt Kitty')
//
// Nostalphone Sketch for the GPO 746 retro replica rotary dial phone. Waits for hook switch and reads dialed number from the rotary dial by scanning the LDR matrix
// The 'flash'-button on the T65 is used to make a direct random selection from all samples, otherwise the first (or only) number
// determines the subdir/bank to be used (each bank has 10 WAVs) and the last number dialed the WAV within this subdirectory/bank.
// Samples are played through DFPlayer (Mini MP3 Player) controlled over SoftwareSerial with RX=10 and TX=11
//
// Expects a SD-card with the correct directory structure of ten banks/subdirs of ten WAVs each
// The bank/subdir 0/ contains the sounds that are played when only one number was dialed
// Otherwise first number is the bank/subdir, last number dialed the sound that is to be played...
// First WAV on SD is played as dial tone, so provide a WAV-file starting with '00' (e.g. '00 Kiestoon350Hz.wav') in the root of the SD card 
//
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

#define SD_ChipSelectPin 10
#define BlinkTime 25000  // Speed of blinking LED during dial phase when receiver is picked up
#define seq_timeout 4000 // Time out before the number dial phase is considered done
#define input_hook  7  // Pin hook switch connected to (purple)
#define led_green   5  // Green LED: lit on power on, flashes when dialing
#define led_red     6  // Red LED: lit when music is played
#define dial_switch A0 // End of dial rotation reached

// Scan trigger (LEDs lighting, end of dial) = A0 - Negative logic, low active!
int dial_columns[] = {A7, A6, A5};   // Intersection analogue read during scan
int scan_lines[] = {A4, A3, A2, A1}; // Analogs used as OUTPUT. Normal=LOW, during scan read one is HIGH
int scan_columns[] = {2, 3, 4};      // Digitals used as OUTPUT. Normal=LOW, during scan read column is LOW
int matrix_read;
int scan_column;
int scan_line;
int dialed;

unsigned long debounce_time_hook  = 0;       // Debounce time for hook switch...
unsigned long debounce_time_dial_switch = 0; // Debounce time end-of-rotary-dial switch...
unsigned long dialing_timeout = 0;           // To keep track of dialing time out...
unsigned long debounceDelay = 50;            // Global debounce time...

bool logging = false; // Serial logging during development...
bool hook_up = false;
bool dialing = false;
bool process_number = false;
bool number_detected = false;

int hook_state;
int pulse_state;
int rand_state;
int last_hook_state = LOW;
int last_dialing_state = HIGH;
int last_io_state_pulse = HIGH;
int last_number_dialed = 0;
int nrs_dialed = 0;
int blink_count = 0; // For the blinking LED...

int io_state_pulse;
int hook_switch_state;

int dial_state;
int dial_switch_state = HIGH;
int last_dial_switch_state = HIGH;

int bank_nr = 0;

SoftwareSerial mySoftwareSerial(10, 11); // RX, TX
DFRobotDFPlayerMini myDFPlayer;
void printDetail(uint8_t type, int value);

void setup() {
  mySoftwareSerial.begin(9600);
  Serial.begin(38400);
      
  pinMode(input_hook, INPUT_PULLUP);
  pinMode(led_red, OUTPUT);
  pinMode(led_green, OUTPUT);
  // Scan trigger (LEDs lighting) = A0
  pinMode(dial_switch, INPUT); // Low active, negative logic...
  
  // Lines to scan...
  for (scan_line=0; scan_line<4; scan_line++) {
    pinMode(scan_lines[scan_line], OUTPUT);
    analogWrite(scan_lines[scan_line], 0); 
  }
  
  // Columns to scan...
  for (scan_column=0; scan_column<3; scan_column++) {
    pinMode(scan_columns[scan_column], OUTPUT);
    digitalWrite(scan_columns[scan_column], LOW); 
  }
  
  // Dial matrix LDR intersection point analogue INPUT columns A1, A2 and A3 
  for (scan_column=0; scan_column<3; scan_column++) {
    pinMode(dial_columns[scan_column], INPUT);
  }

  logSerial("Nostalphone v1.3");
  logSerial("Initializing DFPlayer...");
  if (!myDFPlayer.begin(mySoftwareSerial)) {  //Use softwareSerial to communicate with mp3.
    logSerial("Unable to begin:");
    logSerial("1.Please recheck the connection!");
    logSerial("2.Please insert the SD card!");
    while(true);
  }
  logSerial("DFPlayer Mini connected");

  myDFPlayer.setTimeOut(500); //Set serial communictaion time out 500ms
  
  //----Set volume----
  myDFPlayer.volume(15);  //Set volume value (0~30).
  myDFPlayer.volumeUp(); //Volume Up
  myDFPlayer.volumeDown(); //Volume Down
  myDFPlayer.EQ(DFPLAYER_EQ_NORMAL);
  myDFPlayer.outputDevice(DFPLAYER_DEVICE_SD);  

  if (0) { // Debug diagnostics only: dumps nr of files in each subfolder of the SD card
    Serial.print("1 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(1));
    Serial.print("2 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(2));
    Serial.print("3 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(3));
    Serial.print("4 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(4));
    Serial.print("5 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(5));
    Serial.print("6 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(6));
    Serial.print("7 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(7));
    Serial.print("8 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(8));
    Serial.print("9 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(9));
    Serial.print("10 nr files = ");
    Serial.println(myDFPlayer.readFileCountsInFolder(10));          
  }

  digitalWrite(led_green, LOW);  // turn the green LED on on power on...
  logSerial("Setup done...");   
}

void logSerial(String message) {
  // Handled overhere so logging to serial can easily be switched off...
  if (logging) Serial.println(message);
  return;
}

void loop() {
  // Read hook state...
  hook_switch_state = digitalRead(input_hook);  
  if (hook_switch_state != last_hook_state) {
    debounce_time_hook = millis();
  }
  if ((millis() - debounce_time_hook) > debounceDelay) {
    if (hook_switch_state != hook_state) {
      hook_state = hook_switch_state;
      if (hook_state == LOW) {
        logSerial("Receiver was picked up. Please dial a number...");
        hook_up = true;
        dialing = false;
        bank_nr = -1;
        nrs_dialed = 0;
        // Output dial tone...
        myDFPlayer.play(1);
      } else {
        logSerial("Receiver is put back. Thanx for using the Nostalphone!");
        hook_up = false;
        myDFPlayer.stop(); // End dial tone...
        digitalWrite(led_green, LOW);  // turn the green LED on on power on...
        digitalWrite(led_red, HIGH);   // turn the red LED off (HIGH=off)
      }
    }
  }
  last_hook_state = hook_switch_state;
  
  if (hook_up) { // Dial sequence...
    if ((hook_up || dialing) && digitalRead(led_red)) {
      if (blink_count > BlinkTime) { // Blink LED...
        // Toggle blink...
        digitalWrite(led_green, !digitalRead(led_green));  // Toggle the green LED...
        blink_count = 0;
      }
      blink_count++;
    }
    
    // Be triggered by the end-of-dial switch. Read dial state...
    int dial_switch_state = digitalRead(dial_switch);
    if (dial_switch_state != last_dial_switch_state) {
      debounce_time_dial_switch = millis();
    }
    if ((millis() - debounce_time_dial_switch) > debounceDelay) {
      if (dial_switch_state != dial_state) {
        dial_state = dial_switch_state;
        if (dial_state==LOW) {
          logSerial("Something dialed...");
          if (!dialing) {
            number_detected = false;
            nrs_dialed = 0;
            last_number_dialed = bank_nr = -1;
            myDFPlayer.stop(); // End dial tone...   
          }
          dialing = true;
          digitalWrite(led_red, HIGH);   // turn the red LED off (HIGH=off)
          dialing_timeout = millis(); // Keep resetting timer during dialing...
          
          // Default whole matrix lines to LOW, cols to HIGH...
          for (scan_line=0; scan_line<4; scan_line++)
            analogWrite(scan_lines[scan_line], 0);
          for (scan_column=0; scan_column<3; scan_column++)
            digitalWrite(scan_columns[scan_column], HIGH);         
   
          // Start matrix scan phase...
          dialed = -1;
          for (scan_line=0; scan_line<4; scan_line++) {
            analogWrite(scan_lines[scan_line], 255);
            for (scan_column=0; scan_column<3; scan_column++) {
              digitalWrite(scan_columns[scan_column], LOW); // Active: pull low
              delay(10);
              // Now read this intersection...
              matrix_read = analogRead(dial_columns[scan_column]);      
              digitalWrite(scan_columns[scan_column], HIGH); // Inactive: pull same level as scan-line
              if (matrix_read>100) {
                dialed = (scan_line*3)+(scan_column+1);
                break;
              }
            }
            analogWrite(scan_lines[scan_line], 0);
          }
          if (dialed >= 0) {
            nrs_dialed++;  
            last_number_dialed = dialed;
            if (nrs_dialed==1 && bank_nr<0)
              bank_nr = dialed;
            logSerial((String)nrs_dialed+" - dialed: "+(String)dialed);
          }
        }
      }
    }
    last_dial_switch_state = dial_switch_state;

    if (dialing && ((millis() - dialing_timeout) > seq_timeout)) {
      // Dial time out reached...
      logSerial("Dialing finished ");
      process_number = true;
      dialing = false;
    }

    if (process_number) {
      if (last_number_dialed==11) { // 10=*, 11=0, 12=#
        last_number_dialed = 10; // Remap '0' to song 10
      } else {
        if (last_number_dialed>=10 || last_number_dialed<0) {
          bank_nr = random(9)+1;
          last_number_dialed = random(9)+1;
        } else {
         if (nrs_dialed==1) // Only one non-wildcard number dialed: use default bank 1...
           bank_nr = 1;         
        }
      }
      if (bank_nr>10) bank_nr = 1;

      // Play WAV....
      logSerial("bank_nr "+(String)bank_nr+" song "+(String)last_number_dialed);
      digitalWrite(led_green, HIGH); // Power off green LED...
      digitalWrite(led_red, LOW);    // Turn the red LED on (HIGH=off)

      process_number = false;
      if (myDFPlayer.available()) {
        printDetail(myDFPlayer.readType(), myDFPlayer.read()); //Print the detail message from DFPlayer to handle different errors and states.
      }  
      myDFPlayer.playFolder(bank_nr, last_number_dialed); // Play the song...
    }
    
  } // End hook_up...
}

void printDetail(uint8_t type, int value) {
  switch (type) {
    case TimeOut:
      logSerial("DFPlayer: Time Out!");
      break;
    case WrongStack:
      logSerial("DFPlayer: Stack Wrong!");
      break;
    case DFPlayerCardInserted:
      logSerial("DFPlayer: Card Inserted!");
      break;
    case DFPlayerCardRemoved:
      logSerial("DFPlayer: Card Removed!");
      break;
    case DFPlayerCardOnline:
      logSerial("DFPlayer: Card Online!");
      break;
    case DFPlayerUSBInserted:
      logSerial("DFPlayer: USB Inserted!");
      break;
    case DFPlayerUSBRemoved:
      logSerial("DFPlayer: USB Removed!");
      break;
    case DFPlayerPlayFinished:
      logSerial("DFPlayer: Number:"+(String)value+" Play Finished!");
      break;
    case DFPlayerError:
      switch (value) {
        case Busy:
          logSerial("DFPlayerError: Card not found");
          break;
        case Sleeping:
          logSerial("DFPlayerError: Sleeping");
          break;
        case SerialWrongStack:
          logSerial("DFPlayerError: Get Wrong Stack");
          break;
        case CheckSumNotMatch:
          logSerial("DFPlayerError: Check Sum Not Match");
          break;
        case FileIndexOut:
          logSerial("DFPlayerError: File Index Out of Bound");
          break;
        case FileMismatch:
          logSerial("DFPlayerError: Cannot Find File");
          break;
        case Advertise:
          logSerial("DFPlayerError: In Advertise");
          break;
        default:
          break;
      }
      break;
    default:
      break;
  } 
}
IMG_5366

Draai eens een liedje...

Een leuk project. Het uitlezen van een puls-kiesschijf vergt een compleet andere techniek in de software en een stuk minder ingangen van de Arduino, maar met wat puzzelen bleek het achteraf vrij eenvoudig het LDR-matrix van de GPO746 ook uit te lezen.

Het scannen er van lijkt op het scannen van een membaam-toestenbord of keypad. Door achtereenvolgens alle lijn en kolom combinaties te scannen kan elk individuele LDR-knooppunt bereikt en gelezen worden.

De besproken telefoons hebben inmiddels een goed thuis gevonden en worden nog dagelijks met veel plezier gebruikt.