PLEASE NOTE: This is unofficial documentation of The Wand Company’s Pip-Boy 3000 Mk V. For all official documentation and support please refer to the official manual over at https://www.thewandcompany.com/pip-boy-manual.

The Wand Company’s PipBoy 3000 Mk V is a wonderfully hackable little device which the devlopers have made very easy to people to add their own apps to and make it truly your own.

Creating your own apps

User created appears appear under their own sub-menu under INV > APPS. Any .js file added under the USER/ directory on the SD card will appear in this menu and can be loaded by pressing the side wheel.

To get started with creating apps you first need to get at the micro SD card inside the device. I covered some of the device disassmbly in my log entry Deeper dive into PipBoy 3000 Mk V hardware. I’ve also found it quite useful while working on writing apps to use a Micro SD extension cable to make it easier to move the SD card between my computer and the PipBoy.

Development environments

To simplify testing and development of apps there’s a couple of approaches you can take, the easiest to get started with is to use the Espruino IDE. Connect the Pip-Boy up to your computer the same way you would do the official updates and visit the above linked IDE, on there click the Connect icon in the top left and select “Web Serial” and then select the Pip-Boy to connect to. Using the console in the left of the IDE you can enter commands and interract with the device live.

NOTICE! In the Espruino IDE take great care not to hit any of the “Send to Espruino” icons in the center as you can inadvertently overwrite the firmware already on your device.

If you’re already somewhat familiar with software development, there’s also a plugin for VSCode for Espruino for connecting to your device from VSCode, or alternatively for an even more simple approach, open a direct serial connection to the USB serial port, there are more options and details available in the Espruino Quick Start documentation.

Hello World

This basic “Hello World” app demonstrates how to load an app and print something on the screen. On the SD card create a new text file under the USER directory named helloWorld.js and enter the following:

// Print "Hello World!" on the screen, then 3 seconds later, print some more text.
Pip.typeText("Hello World!").then(() =>
  setTimeout(() => {
    Pip.typeText("Nice app!").then(() => {
      setTimeout(() => {
        // 3 seconds after that, return to the apps menu
        submenuApps()
      }, 3000)
    })
  }, 3000)
)

Insert the SD card back into the PipBoy, I usually at this point also reboot it for good mesaure to ensure it reads the SD card. You should now see a new menu item in the apps menu named helloWorld and selecting it should start the app.

APPINFO

You can also have the app name different to the file name, the below id of helloWorld would map it to an app at USER/helloWorld.js. The name that’s rendered in the UI menu now matches the name in the file which would be saved at APPINFO/helloWorld.json.

{
  "id": "helloWorld",
  "name": "Hello World!"
}

NOTE! The name of the file in APPINFO doesn’t matter, it’s the id in the file that matches it to the app in USER, but it’s worth keeping it the same to keep things logical.

Storage

The built in SD card is a 256MB FAT16 formatted card. This can be replaced though.

Upgrading the SD Card

Storage on the SD card is accessed via the Espruino fs Library which states that in will interface with a FAT32 filesystem on an SD card. You can take a larger SD card, format it as FAT32 and then copy all the files from the built in SD card onto a new larger one and everything appears to work perfectly normally. I tried this with a 16GB SD card and it works completely fine playing a 300MB .wav file, see more about that below in Playing Your Own Audio Files.

Graphics

A lot of the documentation around how the Graphics class works and how things are rendered to screen is best described by the Espruino documentation about the graphics library here.

Here though I will cover the implementation specifics for rendering things on the PipBoy specifically.

Buffers

The contents of the PipBoy display is rendered via 3 different buffers, there is one main Graphics instance a g which is the whole screen and then the buffers render content to various parts of the screen.

bHPage Header. This holds the menu and submenu for navigation in the top portion of the screen
bCMain page content. The main content pane that resides in the space between the header and the footer.
bFPage footer. The bottom portion of the screen that contains the date/time, battery status etc.

Each of these buffers is an instance of the Graphics class and inherits the methods of it. So if you wanted to for example clear the main page content you could use bC.clear().

Display Bounds

As the physical display is actually larger than the opening in the PipBoy case you may find that if you render something at 0,0 you won’t see it as it’ll be physically behind the display shroud.

There is a global variable BGRECT that specifies the bounds of the bC buffer, but you can also use g.getModified() while the system is freshly booted to see the range of pixels that get modified, using a combination of this and trial an error I’ve worked out that the bounds that get the most coverage while maintaing reasonable aspects is:

{ x1: 38, y1: 0, x2: 438, y2: 320 }

This gives us an area that’s 400px wide by 320px high. You can confirm this by drawing a rectangular outline with those bounds with g.clear().drawRect(38, 0, 438, 320) and you should see the outline just touching the sides of the physical housing.

Custom Buffers

I’ve found it quite useful to create my own graphics buffer for my apps to offer some more control over how and when things are rendered. To do this you can create a buffer that fits within the bounds specified previously.

// Buffer is created 400px by 320px, 1 bit per pixel
let bX = Graphics.createArrayBuffer(400, 320, 1, { msb: true })

// Make sure the buffer is empty and then draw an outline of a rectangle to
// fill the buffer, with 20px of margin around each side
bX.clear().drawRect(20, 20, 380, 300)

// Draw a Vault-Tec logo in the middle of the screen
drawVaultTecLogo(200, 110, bX)

// Clear the screen first, then we draw the buffer into the screen, top left
// pixel is at 38,0
g.clear().drawImage(
  { width: 400, height: 320, bpp: 1, buffer: bX.buffer },
  38,
  0
)

Screenshots

For some of the demonstrations in this page I wanted to add screenshots, there are various ways to capture this but the way I used was to dump the buffers into a file on the SD card, the example below dumps the main content buffer into a bitmap file under the directory “SCREENSHOTS”.

require("fs").writeFile("SCREENSHOTS/screenshot.bmp", bC.asBMP())

Audio

Play your own audio files

You can play your own music and audio, but first any files you have must be converted to 16-bit mono channel wave files before they’ll play, this can be achieved easily using ffmpeg.

Example:

ffmpeg -i DiamondCityRadio.ogg -ac 1 -ar 16000  DiamondCityRadio.wav

-ac 1 Sets audio channels to 1.

-ar 16000 Sets audio sampling rate to 16000 Hz.

Files should be placed in the USER/ directory on the SD card. Now in the DATA > MAINTENANCE menu, at the bottom is an option to Play audio files, all your .wav files should appear in here.

Inputs

The main Pip object has events for most of the physical controls, you can add and remove listeners to these events for adding input control to your apps.

EventControlParameters
knob1Left side knob-1 Clockwise rotation, 1 Counter-clockwise rotation, 0 button press
knob2Top knob-1 Clockwise rotation, 1 Counter-clockwise rotation
torchTop button-

Example

An example implementation of using the left knob.

// Remove any existing listeners so ours is the only one
Pip.removeAllListeners("knob1")

// When left knob is rotated or clicked, log the event
Pip.on("knob1", (d) =>
  d !== 0 ? console.log(d > 0 ? "Up" : "Down") : console.log("Clicked")
)

Default Screens

Each of the standard displays have functions that can be called to render them, this can be useful for example when exiting apps to return to known menus etc.

You can fully render whatever the current default menu should be by calling showMainMenu(), this is particularly useful as part of a function call when exiting your apps.

Each one of the built in submenus can also be rendered directly by calling one of the appropriate submenu calls.

submenuBlank        submenuClock
submenuRadio        submenuStatus
submenuConnect      submenuDiagnostics
submenuRad          submenuMap
submenuInvAttach    submenuExtTerminal
submenuApparel      submenuStats
submenuAbout        submenuVideos
submenuAudio        submenuApps
submenuSetAlarm     submenuMaintenance
submenuSetDateTime  submenuSetAlarmTime

UI Elements

There are helper functions for rendering some of the standard UI elements.

Message

The message element renders a Vault-Tec logo in the middle of the display with your chosen message underneath.

https://www.espruino.com/Reference#l_E_showMessage

Example Usage

E.showMessage("Hello World")

Used to present the list based menus like the ones seen in the standard Maintenance pages etc.

https://www.espruino.com/Reference#l_E_showMenu

Example Usage

E.showMenu({
  // Configure a boolean value
  Boolean: {
    value: true,

    // You can use a format function to change how it's presented
    format: (value) => (value ? "On" : "Off"),
  },

  // Set a numeric value with limits
  "Numeric Value": {
    value: 5,
    min: 0,
    max: 10,
    step: 1,

    // Use an onchange function to do something with a new value
    onchange: (newValue, event) => console.log(newValue),
  },

  // Optionally just call a function when pressed
  "Function Call": () => console.log("Function"),
})

Prompt

Prompt the user for an answer to a question.

https://www.espruino.com/Reference#t_l_E_showPrompt

// Basic usage, asks yes or no by default, returns a promise with result
E.showPrompt("Are you sure?").then(console.log)

// Custom usage, change title, buttons and values
E.showPrompt("Choose a prize", {
  title: "Congratulations! ",
  buttons: {
    Stimpak: 1,
    "10mm Ammo": 2,
    Jet: 3,
  },
})

Functions

Pip.isSDCardInserted

Returns true if the SD card is present. Reads SDCARD_DETECT to test presence, an alias of A15.

> Pip.isSDCardInserted()
= true

Pip.getID

Reads the device ID. Device ID is read from memory registers 0x536836624 0x536836632 and 0x536836628.

> Pip.getID()
= "123456Q-12-123B"

Pip.measurePin

Read the average voltage of an analog pin. Used to read the state of the resistor ladder on the mode selection dial.

Parameters

pin Analog pin to read. samples Number of samples to read. Defaults to 10. factor Average is multipled by this factor. Defaults to 2.

> Pip.measurePin(MODE_SELECTOR)
= 0.02578289836 // STAT
> Pip.measurePin(MODE_SELECTOR)
= 1.34515215214 // INV
> Pip.measurePin(MODE_SELECTOR)
= 2.67024886699 // DATA
> Pip.measurePin(MODE_SELECTOR)
= 4.01914486189 // MAP
> Pip.measurePin(MODE_SELECTOR)
= 5.34623848015 // RADIO

Pip.knob1Click

Actions performed when the left knob is turned, plays audio clicks from UI/ROT_V_1.wav and UI/ROT_V_2.wav depending on direction.

Parameters

direction Values greater than 0 play audio 1, less than plays audio 2.

> Pip.knob1Click(1)

Pip.knob2Click

Actions performed when the top knob is turned, plays audio clicks depending on direction. Older versions (< 2v24.206) used UI/PREV.wav but newer uses an audioBuiltin.

Parameters

direction Values greater than 0 play audio 1, less than plays audio 2.

> Pip.knob2Click(1)

Pip.typeText

Prints text onto the screen, the text has a typing effect and plays sounds and prints character by character below a Vault Tec logo.

Parameters

text Text string to display on the screen.

> Pip.typeText("This is some text to demo things!")

Pip.offAnimation

Plays the screen off animation.

> Pip.offAnimation()

Pip.offOrSleep

Power off or sleep the device, can be configured to play or not play various animation sequences.

Parameters

options Object of options, options include forceOff and immediate.

{
  // If true, at the end of the powerOff animation the device will be turned off, if false it will sleep
  forceOff: true/false,

  // If true, skips playing the fade out sequence etc. and will go straight to the powerOff function.
  immediate: true/false
}
> Pip.offOrSleep({ forceOff: false, immediate: false })

Pip.updateBrightness

Sets the brightness of LCD backlight and all LED’s based on the value of Pip.brightness.

> Pip.updateBrightness()

Pip.audioStart

Start playing an audio file from the given path.

Parameters

path Path to the audio file.

> Pip.audioStart("ALARM/Klaxon.wav")

Global Variables

Hardware Pin References

These values just alias the standard Espruino pin definitions to labels that are easier to reference.

LED_RED = E4
LED_GREEN = E5
LED_BLUE = E6
LED_TUNING = E3
BTN_PLAY = A1
BTN_TUNEUP = E1
BTN_TUNEDOWN = E2
BTN_TORCH = A2
KNOB2_A = A10
KNOB2_B = A8
KNOB1_BTN = A3
KNOB1_A = B1
KNOB1_B = B0
BTN_POWER = A0
MEAS_ENB = C4
LCD_BL = B15
VUSB_PRESENT = A9
VUSB_MEAS = A5
VBAT_MEAS = A6
CHARGE_STAT = C5
RADIO_AUDIO = A4
MODE_SELECTOR = A7
SDCARD_DETECT = A15

Fun Notes

Default Date

System date defaults to the date and time the bombs dropped at the start of the Great War.

if (new Date().getFullYear() === 2000) {
  setTime(new Date("2077-10-23T09:47").getTime() / 1000)
}

Factory Test Mode

Holding down torch, play and knob1 will enter factory test mode to test fuctionality.