PEBBLE Portable Audio Recorder

Greetings everyone, and welcome back!

Meet Pebble, a tiny voice recorder designed to look and feel like an actual stone.

The idea behind this project came from a problem I kept running into: I get a ridiculous number of random project ideas throughout the day, and by the time I actually sit down to work on them, half of them are gone. So I wanted a simple little device where I could just hit a button, record an idea, and save it for later.

At the heart of Pebble is the UniHiker M10, an SBC powered by an RK3566 processor, with a built-in touchscreen, microphone, and SD card support. I built a custom power circuit around it and designed the entire enclosure from scratch, taking inspiration from the organic form of a natural stone.

 

HOW THIS WORKS

At the heart of Pebble is a simple recording interface that I designed specifically for the device.

The main screen has a large recording button with a timer displayed on it. When the button is pressed, Pebble starts recording through the built-in microphone, and the timer begins counting so you can see exactly how long you've been recording.

Press the button again, and the recording stops. The audio file is then saved directly to the SD card.

But I also wanted an easy way to access the recordings without having to remove the SD card every time. Since the UniHiker M10 has Wi-Fi connectivity, Pebble can connect to the local network and host its own web interface.

The device displays its local IP address directly on the UI. Enter that IP address into a browser on another device connected to the same network, and you'll be taken to the Pebble web app.

The web app lists all the recordings stored on the SD card, and from there you can play them directly in the browser or download the audio files.

So, all the recordings stay on the SD card, while the web interface simply provides a convenient way to access and manage them over the local network.

DESIGN PROCESS

For the design, I wanted to make an enclosure that really stands out from traditional electronics enclosures. I wanted something organic and natural rather than perfectly symmetrical and boxy.

The first thing that came to mind was a stone or pebble. I really like the irregular, curved forms you find in natural stones, so I picked up a couple of stones from a nearby riverbank and started studying their shapes.

I used them as references and sketched a few forms of my own, experimenting with different curves, proportions, and silhouettes.

After a bit of tinkering with the shapes and drawings, I eventually arrived at the form I was looking for, a simple, organic shape that felt natural enough to pass as a stone while still giving me enough space to fit all the electronics inside.

3D MODEL


Once I had the form I was looking for, I imported my drawing into Fusion 360 and used it as a reference to turn the sketch into a proper CAD model.

From there, I shelled out the enclosure and split it into two halves. I then started fitting everything inside, starting with the UniHiker board, then my custom power circuit, and the lithium cell.

A lot of the work here was making sure all the components fit neatly while still keeping the original stone-like form.

After going through the usual engineering and iteration process, I ended up with a very minimal enclosure that still looks and feels like a stone, rather than a traditional electronics enclosure.

I also wanted to make sure the important ports and controls were still easily accessible without taking the enclosure apart.

On the back of Pebble, I added a small opening that gives direct access to the microSD card. This allows the card to be removed from its holder whenever I need to transfer or manage the recordings.

The USB Type-C port is positioned on the bottom of the enclosure and is used for charging the device. I also placed the power button on the bottom, keeping the controls tucked away while still making them easy to reach when needed.

This keeps the overall stone-like appearance clean while still providing access to everything needed to use and maintain the device.

3D PRINTED PARTS

Once the model was finalized, I exported the mesh files and moved on to printing both halves of the enclosure.

I printed the top and bottom parts using Hyper Grey PLA with a 0.4 mm nozzle, 0.2 mm layer height, and 25% gyroid infill. I used tree supports only where they were needed, mainly on the inside of the enclosure.

Both parts were printed with the bottom side facing the build plate. This meant the outer surfaces were facing upward, giving me a much cleaner finish on the visible top layers.

For the supports, I used a 0.3 mm Z-distance and kept four layers between the support and the actual part. This gave the supports enough separation to remove them easily while still providing reliable support for the overhangs inside the enclosure.

icon PEBBLE.rar 2.73MB Download(0)

HARDWARE - UNIHIKER M10

For the brain of this project, I’m using the UNIHIKER M10, a compact single-board computer from DFRobot. It runs Debian Linux and is built around a quad-core ARM Cortex-A35 processor, with 512 MB of RAM and 16 GB of onboard eMMC storage.

What makes it particularly useful for a project like Pebble is the amount of hardware already packed onto the board; it has a 2.8-inch touchscreen, built-in microphone, Wi-Fi and Bluetooth, along with a microSD card slot and a number of expansion interfaces.

I’ve actually used the UNIHIKER M10 before in my PathFinder project, where I used its Linux and Python environment to build an offline navigation device.

For Pebble, I’m taking advantage of many of the same features, but using them for a much simpler purpose, which is just for recording and managing audio.

DFRobot also provides a really comprehensive Wiki for the M10, with hardware documentation, pinouts, software guides, examples, and Python libraries, which makes developing custom applications on the board much easier. The board comes with a ready-to-use Python environment and supports development through tools such as Jupyter Notebook and VS Code, so I was able to build the entire Pebble interface and functionality using Python.

https://wiki.dfrobot.com/dfr0706-en

CIRCUIT DESIGN

Here we are using the IP5306 Power Management IC Setup, which we have previously used in many of our battery-related projects. This SOIC8 package IC can provide a stable 5V 2.4A from a 3.7V lithium-ion or LIPO cell and also includes many important functions such as overcharging protection, overdischarge, battery fuel level, and charging status.

Below is its datasheet if you want more info on this IC.

https://www.skytech.ir/DownLoad/File/2566_IP5306.pdf

We created the board outline in PCB CAD by following the customized circuit design from our Fusion 360 CAD file. We finalized the board layout by placing all the components in order with the CAD measurements. Once everything lined up, we prepared the final board for fabrication.

PCB ASSEMBLY

  • PCB assembly starts by applying solder paste to each SMD pad using a dispensing syringe. We’re using standard 63/37 Sn-Pb solder paste.
  • Once that’s done, each SMD component is placed in position using ESD-safe tweezers.
  • The board then goes onto a mini reflow hotplate, which heats it from below. As soon as the temperature hits around 200°C, the solder paste melts, and the components are soldered in place.
  • Next, we move on to the through-hole components, which begins by placing the push button, Type C Port, and JST Connector in their position.
  • After placing them, we flip the PCB and solder the through-hole pads using a soldering iron.

POWER SOURCE

For power, I’m using a 3.7V 600 mAh Li-Po cell, which works really well for this setup. Originally, I planned to use a 14500 3.7V 600 mAh Li-ion cell, but it wasn’t readily available, so I switched to a Li-Po instead.

This wasn’t really a compromise, since both cells have the same nominal voltage and capacity, and the Li-Po is also considerably thinner, making it easier to fit inside the enclosure.

The battery connects to the custom power circuit through a JST connector. To turn Pebble on, I simply press the vertical push button mounted on the circuit. The circuit then powers up, and the indicator LED lights up to show that the power system is active and providing a stable 5V output to the UniHiker M10.

UNIHIKER M10 & POWER CIRCUIT WIRING

One small challenge I ran into was that the UniHiker M10 doesn’t have a dedicated 5 V or VIN input pin that I could directly use for powering the board. Since the board is normally powered through its USB Type-C port, I decided to use that existing power path instead.

I plugged a USB Type-C cable into the UniHiker and used a multimeter to trace the PCB and find the test pads that were carrying the 5 V supply from the USB port. Once I identified the correct pads, I soldered two wires directly to them.

These two wires were then connected to the 5 V output of my custom power circuit, effectively giving the UniHiker the same 5 V supply it would receive through the USB Type-C port.

With everything connected, pressing the power button on the power circuit turns the system on, powering up the UniHiker and bringing the entire Pebble setup to life.

CODE

Check out my GitHub for full code.

https://github.com/razorArnov/UniRecorder

Pebble is two Python scripts running side-by-side on the UNIHIKER

recorder.py- Drives the touchscreen UI, listens for button presses, records audio

webpage.py - Hosts a Flask web server on port 5000, your wireless vault for playing back and downloading recordings

You tap the stone on screen; it pulses and counts up while recording; tap again, it saves on your phone; open the IP address shown at the bottom of the device screen, and your recording appears as a scrollable card ready to download.

DEVICE UI

1.Setting Up the Display

The UNIHIKER's 'unihiker' library provides a 'GUI' class for drawing objects on the screen. The recorder draws a marble background using layered diagonal lines over a flat color:

from unihiker import GUI, Audio

gui = open_gui()
audio = Audio()

# Color palette — tuned to look like natural stone
MARBLE = "#ECEDEF"
MARBLE_HI = "#F6F5F3"
MARBLE_LO = "#DCDFE4"
STONE = "#BFC5CD"
ACCENT = "#3F6B8C" # recording blue

# Draw marble background
gui.draw_rect(x=0, y=0, w=240, h=320, fill=MARBLE, color=MARBLE)

# Simulate veining with offset lines
for x0, y0, x1, y1, col in (
(-30, 70, 150, -20, MARBLE_LO),
(-10, 130, 110, 10, MARBLE_HI),
( 90, 340, 260, 170, MARBLE_LO),
):

2.The Pebble Button (Skeuomorphic Circle)

Instead of a flat button, the "pebble" is built from three stacked circles: a shadow circle (bottom), a highlight circle top-left, and the main body center. This creates the illusion of a raised, 3D stone

CX, CY = 120, 148 # center of screen
R_IDLE = 50

peb_cast = gui.draw_circle(x=CX+5, y=CY+5, r=R_IDLE,
fill=STONE_CAST, color=STONE_CAST, onclick=on_tap)
peb_lift = gui.draw_circle(x=CX-5, y=CY-5, r=R_IDLE,
fill=WHITE, color=WHITE, onclick=on_tap)
peb_body = gui.draw_circle(x=CX, y=CY, r=R_IDLE,
fill=STONE, color=STONE_LO, onclick=on_tap)

# Small specular highlight — top-left, like a light source
peb_spec = gui.draw_circle(x=CX-19, y=CY-19, r=11,
fill=STONE_HI, color=STONE_HI, onclick=on_tap)

3.Recording Logic

Toggle recording is handled in a single function. When recording starts, the accent color #3F6B8C, a slate blue, floods the pebble, and the main loop starts animating a pulse:

def toggle_recording():
global is_recording, radius, growing, started_at, pebbles

is_recording = not is_recording

if is_recording:
filename = new_filename(next_index())
audio.start_record(os.path.join(SAVE_DIR, filename))
started_at = time.monotonic()
status_text.config(text="R E C O R D I N G", color=ACCENT)
paint_pebble(R_IDLE, ACCENT, "#6E93B2", WHITE)
else:
audio.stop_record()
pebbles = count_pebbles()
status_text.config(text="S A V E D", color=ACCENT_DK)
paint_pebble(R_IDLE, STONE, STONE_HI, INK)

The main loop breathes the pebble in and out while the recording radius oscillates between R_MIN = 46 and R_MAX = 62 at 20 fps:

R_MIN, R_MAX = 46, 62

while True:
now = time.monotonic()

if is_recording:
if growing:
radius += 2
if radius >= R_MAX:
growing = False
else:
radius -= 2
if radius <= R_MIN:
growing = True

paint_pebble(radius, ACCENT, "#6E93B2", WHITE)

elapsed = int(now - started_at)
peb_label.config(text="%02d:%02d" % (elapsed // 60, elapsed % 60))

time.sleep(0.05)

4.Intelligent File Naming

Files are saved to /media/mmcblk0p1/Voice_Memos on the MicroSD card. The filename includes an auto-incrementing index and a wall-clock timestamp, but only if the device clock looks sane year ≥ 2001, meaning the device actually synced NTP:

def new_filename(index):
if time.localtime().tm_year >= 2001:
return "pebble_%04d_%s.wav" % (index, time.strftime("%Y%m%d-%H%M%S"))
return "pebble_%04d.wav" % index

This prevents two problems: overwrites across reboots (the index), and garbage timestamps from a drifted clock (the year guard).

The index is determined by scanning existing filenames with a regex:

NAME_RE = re.compile(r"^pebble_(\d{4})")

def next_index():
highest = 0
for name in os.listdir(SAVE_DIR):
m = NAME_RE.match(name)
if m:
highest = max(highest, int(m.group(1)))
return highest + 1

Physical Button Support

The A button on the UNIHIKER side is wired up with a single line:

gui.on_a_click(on_tap)

A debounce guard prevents double-fires from quick taps:

last_tap = 0.0

def on_tap():
global last_tap
now = time.monotonic()
if now - last_tap < 0.4:
return
last_tap = now
toggle_recording()

THE WEB DASHBOARD

1.Flask Server with HTTP Range Support

The web server is a minimal Flask app. The key detail is passing 'conditional=True' to 'send_from_directory' — this enables HTTP range requests, which is what lets the browser's

@app.route('/audio/')
def serve_audio(filename):
return send_from_directory(SAVE_DIR, filename, conditional=True)

2.Listing Recordings

The 'list_recordings()' function reads the SD card, extracts timestamps, and returns entries sorted newest-first:

def list_recordings():
entries = []
for name in os.listdir(SAVE_DIR):
if name.startswith(".") or not name.lower().endswith(AUDIO_EXTS):
continue
st = os.stat(os.path.join(SAVE_DIR, name))

# Prefer the wall-clock baked into the filename
dt = stamp_from_name(name)
exact = dt is not None
if dt is None:
dt = datetime.fromtimestamp(st.st_mtime)

entries.append({
"name": name,
"sort": dt.timestamp() if exact else st.st_mtime,
"stamp": st.st_mtime,
"exact": exact,
"day": dt.strftime("%d %b"),
"clock": dt.strftime("%I:%M %p").lstrip("0").lower(),
"size": human_size(st.st_size),
})

entries.sort(key=lambda e: e["sort"], reverse=True)
return entries

3.Client-Side Timezone Handling

The UNIHIKER runs on UTC or has no correct timezone at all. If timestamps were formatted server-side, everyone accessing the dashboard from a different timezone would see wrong times. Instead, raw epoch timestamps are passed to the browser as data-ts attributes and formatted in JavaScript using the viewer's own locale:

var nodes = document.querySelectorAll("[data-ts]");

for (var i = 0; i < nodes.length; i++) {
var el = nodes[i];
var ts = parseFloat(el.getAttribute("data-ts"));
var d = new Date(ts * 1000);

if (el.getAttribute("data-part") === "day") {
el.textContent = d.toLocaleDateString(undefined, { day: "2-digit", month: "short" });
} else {
el.textContent = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
}
}

4.Clock Drift Warning

The UNIHIKER M10 has no battery-backed RTC. If it boots offline, its clock defaults to somewhere in 2019. The dashboard detects this by comparing the server's reported time to Date.now() in the browser. If the gap is more than 3 minutes, a warning banner appears:

var serverNow = parseFloat(document.body.getAttribute("data-server-now"));
var driftSec = (Date.now() / 1000) - serverNow;

if (Math.abs(driftSec) > 180) {
var span = /* format hours and minutes */;
warn.textContent = "Device clock is " + span + " " +
(driftSec > 0 ? "behind" : "ahead") + " — timestamps may be off";
warn.className = "clock-warn on";
}

5.The Marble UI (CSS)

The web UI uses vanilla CSS with SVG-based fractal noise textures rendered inline as data URIs. No canvas, no WebGL, just CSS filter magic baked into the markup:

/* Marble background: gradient + two noise layers at different scales */
html {
background-image:
radial-gradient(120% 80% at 15% 0%, rgba(255,255,255,.95) 0%, transparent 55%),
linear-gradient(155deg, var(--marble-hi) 0%, var(--marble-mid) 48%, var(--marble-lo) 100%);
}

/* Coarse veining layer */
body::before {
background-image: url("data:image/svg+xml,...feTurbulence baseFrequency='0.006 0.012'...");
opacity: .16;
mix-blend-mode: multiply;
}

/* Fine stone grain */
body::after {
background-image: url("data:image/svg+xml,...feTurbulence baseFrequency='0.85'...");
opacity: .42;
mix-blend-mode: overlay;
}

Each recording card is a neumorphic stone slab, a raised surface using inset shadows for the carved audio player well and outset shadows for the card lift:

.pebble-slab {
background: linear-gradient(150deg,
var(--stone-hi) 0%, var(--stone) 38%,
var(--stone-belly) 72%, var(--stone-lo) 100%);
border-radius: 32px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,.62), /* top edge catch-light */
inset 0 -1px 0 rgba(70,78,88,.20), /* bottom edge shadow */
13px 13px 26px var(--cast), /* drop shadow */
-8px -8px 20px var(--lift); /* ambient lift */
}

.audio-well {
background: var(--stone-inset);
box-shadow:
inset 5px 5px 11px rgba(104,113,125,.55), /* carved in */

SETUP & RUNNING

1. Copy the files to your UNIHIKER

Connect via SSH or the UNIHIKER's file manager and drop both scripts into your home directory:

/home/
├── recorder.py
└── webpage.py

2. Install Flask

pip install flask

3. Start the web server (background)

python3 webpage.py &

4. Start the recorder

python3 recorder.py

Once the recorder is running, the bottom of the screen shows the web server URL. Open it on any device connected to the same Wi-Fi network.

5.Autostart on Boot

To have both scripts launch automatically when the UNIHIKER powers on, add them to /etc/rc.local or use the UNIHIKER's built-in "Start on Boot" option in Mind+ / the UNIHIKER app.

SURFACE FINISH PROCESS—SANDING


Now comes the surface finishing process, where we start working on the printed parts to get rid of the visible 3D-print layer lines.

For the first stage, I’m using 100-grit sandpaper along with a sandpaper holder. I start sanding the outer surfaces of both parts, making sure to work evenly across the entire surface.

The goal at this stage is to completely remove the visible layer lines and flatten out the surface.

SURFACE FINISH PROCESS—PRIMER

Next comes the priming process. For this, I’m using Epoxy Grey Spray Primer and applying it in multiple thin coats over both parts of the enclosure.

I applied three coats in total. After the first coat, I let the primer dry for about an hour before applying the second coat. I then gave it another hour to dry before applying the third and final coat.

SURFACE FINISH PROCESS—PAINT

Now comes the fun part, which is painting the enclosure.

For this, I mainly used two acrylic colors: black and white. The goal was to give Pebble a dark, slightly greyish appearance that would resemble the natural color and texture of a stone.

I started by giving the entire enclosure a coat of black paint. Once that was done, I used white paint to add a few random strokes and highlights across the surface. I wasn't trying to make the color perfectly uniform; the idea was to create an irregular greyish pattern, similar to the natural variations you find on a stone.

After a bit of experimenting, I was happy with the final stone-like finish. To protect the paint and make the finish more durable, I applied two coats of acrylic conformal spray, leaving around 30 minutes between each coat.

This gave the enclosure a protective layer over the paint and helped make the final finish much more durable and long-lasting.

FINAL ASSEMBLY

The final assembly begins by adding a piece of double-sided tape to the designated area for the lithium battery. This keeps the Li-Po cell securely in place inside the enclosure.

Next, I place the custom power circuit into its position and mount the Li-Po cell over the double-sided tape. The power circuit is pressure-fitted into the enclosure, and the ribs designed around it keep the circuit securely locked in place, so there’s no room for it to move around.

The UniHiker M10 is then placed into its designated position in the other half of the enclosure, with all the wiring connected and tucked neatly inside.

Once everything is in place, both halves of the enclosure are brought together and secured using four M2 screws.

And with that, the assembly is finally complete

RESULT

And here’s the end result of the build: Pebble, a fully functional voice recorder disguised as a stone.

What started as a simple idea for capturing random project thoughts turned into a complete little device with its own custom enclosure, power system, recording interface, and web app.

CONCLUSION

And that brings us to the end of the Pebble build.

The device works really well, and I’m quite happy with how it turned out. You might be thinking that using a UniHiker M10 for a simple voice recorder is a bit of overkill and honestly, I completely agree. It definitely is.

For a future version, I’d like to tackle that by replacing the UniHiker entirely with something smaller, cheaper, and more purpose-built. That would probably mean designing a custom board and doing some R&D on the electronics, but that’s a project for another day.

For now, Pebble is complete, and I’m really happy with the final result.

If you have any questions about the project, feel free to drop them in the comments or DM me on Instagram. I’m quite active there and always happy to talk about my projects.

Thanks for making it this far, and I’ll be back with a new project very soon.

Peace!

License
All Rights
Reserved
licensBg
0