WiPhone Programming Instructions

Prerequisites

Hardware

For compiling and editing WiPhone firmware, you will need a computer capable of running the Arduino Desktop IDE and a microUSB cable. A modern Linux, MacOS or Windows operating system should work.

Software

The following software packages are required to compile the WiPhone firmware:

  1. Arduino Desktop IDE: https://www.arduino.cc
  2. Arduino core for WiPhone: https://wiphone.io/static/releases/arduino_platforms/WiPhone0.1.0.zip
  3. An Arduino plugin for uploading files to the ESP32 file system: https://github.com/me-no-dev/arduino-esp32fs-plugin

If you want to contribute to the firmware, you should also have the Git source-control management tool: https://git-scm.com/

Installing The Arduino Desktop IDE

Download the installer for your platform from the official Arduino website download page: https://www.arduino.cc/en/Main/Software

Follow the installation procedure for your operating system:

In all cases you can check the official instructions for more details.

Installing WiPhone-Arduino using Arduino IDE Boards Manager

If your Arduino IDE is recent enough (ver. 1.8+), you can install Arduino-ESP32 using the Arduino IDE's Boards Manager:

  1. Start the Arduino Desktop IDE.

  2. Open the Preferences window.

  3. Enter "https://wiphone.io/static/releases/arduino_platforms/package_WiPhone_index.json"(without quotes) into the Additional Board Manager URLs field.
    Note: You can add multiple URLs, separating them with commas.
    ../../_images/arduino_preference.png
  4. Open the Boards Manager... window by navigating to Tools > Board: >Boards Manager...

  5. Search for "wiphone"and install the WiPhone package by the WiPhone Team.

    ../../_images/arduino_boardmanager.png

Choosing a board type

After installing WiPhone-Arduino, select the WiPhone board by navigating to Tools > Board: ... menu.

../../_images/arduino_board.png

Warning

Changing the partition scheme will overwrite the internal flash file system (SPIFFS) causing loss of things like the phonebook and SIP accounts. While these can be easily restored if you have a backup, you may want to stick to a single partition scheme early in the development process to avoid data loss for frequently changing files.

Installing The Arduino Plugin for Uploading Files

This plugin is required to upload data files to the WiPhone's internal flash (SPIFFS). It is needed, for example, to load the ringtone file and background screen. It also formats the SPIFFS partition of the ESP32 to allow storing WiPhone data (like phonebook, SIP accounts, and settings) into the internal flash.

Warning

Using this plugin to load files into a WiPhone will overwrite ALL existing files in the SPIFFS partition, causing irreversible data loss. Use this only if you know what you are doing!

Installation procedure:

  1. Download the plugin archive:
    https://github.com/me-no-dev/arduino-esp32fs-plugin/releases/download/1.0/ESP32FS-1.0.zip
  2. Extract the archive and copy the extracted directory ESP32FS into the tools subdirectory of Arduino sketchbook directory:
    • On Linux: create directory ~/Arduino/tools/ (you can run "mkdir -p ~/Arduino/tools/"in terminal) and copy the ESP32FS direcory into it.
    • On MacOS: create directory ~/Documents/Arduino/tools/ (you can run "mkdir -p ~/Documents/Arduino/tools/"in terminal) and copy the ESP32FS direcory into it.

For more details, you may also consult the official installation instructions: https://github.com/me-no-dev/arduino-esp32fs-plugin#Installation

Obtaining A Copy Of The Firmware

Firmware is currently being reviewed by Early Testers. In the future this section will include a download link.

Compiling And Uploading Firmware

After obtaining firmware, make sure all the project files are stored in a directory called "WiPhone". Then, to compile and upload the firmware to WiPhone:

  1. Connect WiPhone to your computer with a microUSB cable.
  2. Open the file WiPhone.ini from the project directory in the Arduino IDE.
  3. Press button "Upload"in the top left corner of the Arduino IDE window (the button looks like a right arrow).

Uploading Data Files

To permanently store files in the WiPhone's internal flash file system (SPIFFS).

To upload data files from the "WiPhone/data"subdirectory to SPIFFS, do the following:

  1. Make sure that the plugin for uploading files to ESP32 is installed(otherwise, see the dedicated section above).
  2. Navigate to the "Tools"menu in the Arduino IDE and press "ESP32 Sketch Data Upload".

What's In The Project Directory

Some notable files and subdirectories from the source directory:

  • WiPhone.ino: Arduino project file: a C++ file with the main loop and the mainphone logic
  • GUI.h/GUI.cpp: main GUI logic files (widgets and apps are defined here)
  • tinySIP.h/tinySIP.cpp: a minimal implementation of the SIP protocol, optimized for small footprint
  • src/
    directory for code that is not in active development (these files are required for the WiPhone's operation, but will not be opened by Arduino IDE by default, unlike files in the main directory)
    • src/TFT_eSPI/: an LCD graphics library for the ESP32; includes sprites and font rendering capabilities
    • src/VoIP/: audio codecs and other definitions for VoIP
    • src/assets/: static data components (such as fonts, icons, background image, etc.)
    • src/drivers: code that operates different integrated circuits (ICs) of the WiPhone
  • data/: files to be loaded into WiPhone’s internal flash file system (SPIFFS)
  • tools/: custom Python scripts to generate "assets", namely 3-bit antialiased fonts and icons
  • other WiPhone source files in the main directory

Developing Custom "Apps"

Developing custom apps in WiPhone's Arduino firmware should be possible with a moderate understanding of C++. This section covers a few of the less obvious aspects.

Steps for adding a custom app

  1. Declare a unique ID for the new app.
    This is done in the ActionID enumeration:
    Find "typedef enum ActionID"in file GUI.h. Add a unique name below the line "GUI_BASE_APP = 0x4000". For example, "GUI_APP_MYAPP".

    GUI.h:

    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    // Menu actions
    typedef enum ActionID : uint16_t {
      NO_ACTION = 0,
      GUI_ACTION_MAINMENU,
      GUI_ACTION_SUBMENU,
      GUI_ACTION_RESTART,
    
      // Specific applications
      GUI_BASE_APP = 0x4000,      // application flag
    
      // My Section
      GUI_APP_MYAPP,      // add your app like this
    
      // Interface
      GUI_APP_MENU,
      GUI_APP_CLOCK,
    
  2. Add your app ID into the menu.
    Find ''GUIMenuItem menu'' in file GUI.h.
    Increase size of the array by 1 and add a line of the following form into the definition of the array "menu"

    { XX, YY, "Menu Name", "", "", GUI_APP_MYAPP }
    
    • XX - should be a unique ID of the menu item (for simplicity, it should be the current size of the array)
    • YY - is ID of the menu in which you want your app to appear.


    GUI.h:

    2155
    2156
    2157
    2158
    2159
    2160
    2161
    2162
    2163
    2164
    2165
    2166
    2167
    2168
    2169
    2170
    2171
    2172
    2173
    2174
    2175
    2176
    2177
    2178
    2179
    2180
    2181
    2182
    GUIMenuItem menu[36] PROGMEM = {  // increment size by one to add a new app
    
      // TODO: button names can be removed
    
      { 0, -1, "Clock", "Menu", "", GUI_APP_CLOCK },
    
      { 1, 0, "WiPhone", "Select", "Back", GUI_ACTION_SUBMENU },
    
      // Main menu items
      // TODO: call log (icons: Call_log_b/Call_log_w)
      { 2, 1, "Phonebook", "", "", GUI_APP_PHONEBOOK },
      { 20, 1, "Messages", "", "", GUI_APP_MESSAGES },
      { 3, 1, "Tools", "Select", "Back", GUI_ACTION_SUBMENU },
      { 4, 1, "Games", "Select", "Back", GUI_ACTION_SUBMENU },
      { 5, 1, "Settings", "Select", "Back", GUI_ACTION_SUBMENU },
      { 13, 1, "Reboot", "", "", GUI_ACTION_RESTART },
    
      // Tools (3)
      { 25, 3, "MP3 player", "", "", GUI_APP_MUSIC_PLAYER },
      { 31, 3, "Audio recorder", "", "", GUI_APP_RECORDER },
      { 14, 3, "Scan WiFi networks", "", "", GUI_APP_NETWORKS },    // duplicate from below
      { 7, 3, "Note page", "", "Back", GUI_APP_NOTEPAD },
      { 21, 3, "UDP sender", "", "", GUI_APP_UDP },
      { 28, 3, "Development", "Select", "Back", GUI_ACTION_SUBMENU },
    
      // Development (28)
      { 36, 28, "My App", "", "", GUI_APP_MYAPP },
      { 27, 28, "Diagnostics", "", "", GUI_APP_DIAGNOSTICS },
    
  3. Instantiate your app. This is, basically, just letting the GUI know how to call your app’s constructor.

    Find definition of method "GUI::enterApp()"in file GUI.cpp and add code to create a new object out of your app class. (Do it similarly to other apps.)

    1126
    1127
    1128
    1129
    1130
    1131
    1132
    1133
    1134
    1135
    1136
    1137
    1138
    1139
    1140
    1141
    void GUI::enterApp(ActionID_t app) {
      GUI_DEBUG("entering app");
    
      // Free dynamic memory
      cleanAppDynamic();
      flash.end();
    
      // Change to new app
      switch(app) {
      case GUI_APP_MYAPP:
        runningApp = new MyApp(audio, *screen, state, header, footer);
        break;
      case GUI_APP_MENU:
        state.setInputState(InputType::Numeric);
        menuDrawn = false;
        break;
    
  4. Declare the app class and methods.

    The easiest way to do this is to find an existing app similar to the one you want to build, copy its code, changing the class name and code to do the things you want.
    The app class declaration goes in GUI.h and method definitions go in GUI.cpp.

    The app class must derive from ''WiPhoneApp'' or of its derivatives (like "WindowedApp"or "FocusableApp", or both).

    Make sure the class method "getId"returns the ID that you created in the first step.

    Methods go in GUI.cpp. Each app defines two main methods: "processEvent()"and "redrawScreen()".

    "processEvent(EventType event)"is the method that is called by GUI to allow your app process events and update its internal state accordingly. The events are, for example, button presses, timers (as requested by your app) or scheduled events (as scheduled by your app).

    "redrawScreen(bool redrawAll)"is the method that is called by GUI to allow your app to redraw the screen partially and/or telling your app that it should redraw the screen entirely (like after the screen was redrawn by some other app, like a call or a screen lock).

    The MyApp demo is pasted below.

    GUI.h

    1043
    1044
    1045
    1046
    1047
    1048
    1049
    1050
    1051
    1052
    1053
    1054
    1055
    1056
    1057
    1058
    1059
    1060
    1061
    1062
    class MyApp : public WindowedApp, FocusableApp {
     public:
      MyApp(Audio* audio, LCD& disp, ControlState& state, HeaderWidget* header, FooterWidget* footer);
      virtual ~MyApp();
      ActionID_t getId() { return GUI_APP_MYAPP; };
      appEventResult processEvent(EventType event);
      void redrawScreen(bool redrawAll=false);
    
     protected:
      Audio* audio;
    
      bool screenInited = false;
    
      // WIDGETS
      RectWidget* clearRect;
      RectIconWidget* iconRect;
    
      LabelWidget*  demoCaption;
      LabelWidget*  debugCaption;
    };
    

    GUI.cpp

    1466
    1467
    1468
    1469
    1470
    1471
    1472
    1473
    1474
    1475
    1476
    1477
    1478
    1479
    1480
    1481
    1482
    1483
    1484
    1485
    1486
    1487
    1488
    1489
    1490
    1491
    1492
    1493
    1494
    1495
    1496
    1497
    1498
    1499
    1500
    1501
    1502
    1503
    1504
    1505
    1506
    1507
    1508
    1509
    1510
    1511
    1512
    1513
    1514
    1515
    1516
    1517
    1518
    1519
    1520
    1521
    1522
    1523
    1524
    1525
    1526
    1527
    1528
    1529
    1530
    1531
    1532
    1533
    1534
    1535
    1536
    1537
    1538
    1539
    1540
    1541
    1542
    1543
    1544
    1545
    1546
    1547
    1548
    1549
    1550
    1551
    1552
    1553
    1554
    1555
    1556
    1557
    1558
    1559
    1560
    1561
    1562
    1563
    1564
    1565
    1566
    1567
    1568
    1569
    1570
    1571
    1572
    1573
    1574
    1575
    1576
    1577
    1578
    1579
    1580
    1581
    1582
    1583
    1584
    1585
    1586
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - -  MyApp demo app  - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    
    MyApp::MyApp(Audio* audio, LCD& lcd, ControlState& state, HeaderWidget* header, FooterWidget* footer)
           : WindowedApp(lcd, state, header, footer), FocusableApp(2), audio(audio)
    {
      GUI_DEBUG("MyApp create");
      const char* s;
    
      // Create and arrange widgets
      header->setTitle("MyApp Demo");
      footer->setButtons("Yes", "No");
      clearRect = new RectWidget(0, header->height(), lcd.width(), lcd.height() - header->height() - footer->height(), WP_COLOR_1);
    
      // State caption in the middle
      const uint16_t spacing = 4;
      uint16_t yOff = header->height() + 26;
      demoCaption = new LabelWidget(0, yOff, lcd.width(), fonts[AKROBAT_BOLD_20]->height(),
                                     "Hello World", WP_ACCENT_S, WP_COLOR_1, fonts[AKROBAT_BOLD_20], LabelWidget::CENTER);
      yOff += demoCaption->height() + (spacing*2);
    
      // Make an icon
      iconRect = new RectIconWidget((lcd.width()-50)>>1, yOff, 50, 50, WP_ACCENT_1, icon_person_w, sizeof(icon_person_w));
      yOff += iconRect->height() + (spacing*2);
    
      /*// Name and URI above
      s = controlState.calleeNameDyn!=NULL ? (const char*) controlState.calleeNameDyn : "";
      nameCaption =  new LabelWidget(0, yOff, lcd.width(), fonts[AKROBAT_EXTRABOLD_22]->height(), s, WP_COLOR_0, WP_COLOR_1, fonts[AKROBAT_EXTRABOLD_22], LabelWidget::CENTER);
      yOff += nameCaption->height() + spacing;
      s = controlState.calleeUriDyn!=NULL ? (const char*) controlState.calleeUriDyn : "";
      uriCaption =  new LabelWidget(0, yOff, lcd.width(), fonts[AKROBAT_BOLD_20]->height(), s, WP_DISAB_0, WP_COLOR_1, fonts[AKROBAT_BOLD_20], LabelWidget::CENTER);*/
    
      // Debug string: shows some simple debug info
      //yOff += uriCaption->height() + 20;
      //s = controlState.lastReasonDyn != NULL ? (const char*) controlState.lastReasonDyn : "";
      s = "I'm Awesome!";
      debugCaption = new LabelWidget(0, yOff, lcd.width(), fonts[AKROBAT_BOLD_16]->height(), s, WP_DISAB_0, WP_COLOR_1, fonts[AKROBAT_BOLD_16], LabelWidget::CENTER);
    
      //reasonHash = hash_murmur(controlState.lastReasonDyn);
    }
    
    MyApp::~MyApp() {
      GUI_DEBUG("destroy MyApp");
    
      delete demoCaption;
      delete debugCaption;
      //delete nameCaption;
      //delete uriCaption;
    }
    
    appEventResult MyApp::processEvent(EventType event) {
      GUI_DEBUG("processEvent MyApp");
      appEventResult res = DO_NOTHING;
    
      if (LOGIC_BUTTON_BACK(event)) {
    
        demoCaption->setText("Back Button");
        res |= REDRAW_SCREEN;
    
      } else if (LOGIC_BUTTON_OK(event)) {
    
        demoCaption->setText("OK Button");
        footer->setButtons("OH", "NO");
        res |= REDRAW_SCREEN | REDRAW_FOOTER;
    
      } else if (event == WIPHONE_KEY_UP || event == WIPHONE_KEY_DOWN) {
    
        int8_t earpieceVol, headphonesVol, loudspeakerVol;
        audio->getVolumes(earpieceVol, headphonesVol, loudspeakerVol);
        int8_t d = event == WIPHONE_KEY_UP ? 6 : -6;
        earpieceVol += d;
        headphonesVol += d;
        loudspeakerVol += d;
        audio->setVolumes(earpieceVol, headphonesVol, loudspeakerVol);
        audio->getVolumes(earpieceVol, headphonesVol, loudspeakerVol);
        char buff[70];
        snprintf(buff, sizeof(buff), "Speaker %d dB, Headphones %d dB, Loudspeaker %d dB",  earpieceVol, headphonesVol, loudspeakerVol);
        debugCaption->setText(buff);
    
      }
    
      return res;
    }
    
    void MyApp::redrawScreen(bool redrawAll) {
      GUI_DEBUG("redrawScreen MyApp");
    
      if (!screenInited || redrawAll) {
        GUI_DEBUG("redraw all");
        // Initialize screen
        ((GUIWidget*) clearRect)->redraw(lcd);
    
        ((GUIWidget*) iconRect)->redraw(lcd);
        ((GUIWidget*) demoCaption)->redraw(lcd);
        ((GUIWidget*) debugCaption)->redraw(lcd);
        //((GUIWidget*) nameCaption)->redraw(lcd);
        //((GUIWidget*) uriCaption)->redraw(lcd);
    
        //lcd.fillRect(95, 240, 50, 20, WP_ACCENT_1);     // DEBUG: very strange bug with white pixels over black border
    
      } else {
    
        // Refresh only updated labels
        if (demoCaption->isUpdated()) {
          GUI_DEBUG("stateCaption updated");
          ((GUIWidget*) demoCaption)->redraw(lcd);
        }
        if (debugCaption->isUpdated()) {
          GUI_DEBUG("debugCaption updated");
          ((GUIWidget*) debugCaption)->redraw(lcd);
        }
        /*if (nameCaption->isUpdated()) {
          GUI_DEBUG("nameCaption updated");
          ((GUIWidget*) nameCaption)->redraw(lcd);
        }
        if (uriCaption->isUpdated()) {
          GUI_DEBUG("uriCaption updated");
          ((GUIWidget*) uriCaption)->redraw(lcd);
        }*/
      }
      screenInited = true;
    }