if !btnA.Get() { // LOW = pressed (active LOW with pull-up)
led.High()
} else {
led.Low()
}
time.Sleep(time.Millisecond * 10)
}
}
```
```sh
tinygo flash -target nicenano ./basics/step1
```
Hold button A — the LED turns on. Release it — the LED turns off.
**Key concepts**
-`machine.PinInputPullup` — the pin floats HIGH internally; pressing the button connects it to GND → LOW.
-`!btnA.Get()` — because the logic is inverted (active LOW), we negate the reading.
**Challenge:** modify the code so that the LED turns on when button **B** is pressed instead.
---
### step2 — WS2812 RGB LEDs
**Goal:** drive the two addressable RGB LEDs.
WS2812 (SK6812MINI-E) LEDs are controlled with a single data wire using a timed pulse protocol. The `ws2812` driver handles the timing; you just provide colors.
The two LEDs alternate between red and green every 300 ms.
**Key concepts**
-`color.RGBA{R, G, B, A}` — standard Go color type; A (alpha) is always 255 for LEDs.
-`leds.WriteColors(slice)` — pushes the entire color slice to the strip in one call.
**Challenge:** add a third color (blue) and cycle through all three.
---
### step3 — WS2812 LEDs + buttons
**Goal:** combine inputs and outputs — each button sets a different LED color.
```sh
tinygo flash -target nicenano ./basics/step3
```
- Press **A** → both LEDs turn red.
- Press **B** → both LEDs turn blue.
- Press the **rotary button** → both LEDs turn green.
- Release all → LEDs stay on the last color.
**Key concepts**
- Multiple inputs read in the same loop.
- State is kept across loop iterations with the `c` variable.
---
### step3b — Rainbow LEDs
**Goal:** generate smooth color transitions (hue wheel) and let buttons scroll through them.
The `getRainbowRGB` function maps a `uint8` (0–255) to a point on the RGB color wheel, dividing it into three 85-step segments: red→green, green→blue, blue→red.
```sh
tinygo flash -target nicenano ./basics/step3b
```
- Hold **A** → hue advances (warm colors).
- Hold **B** → hue recedes (cool colors).
- The two LEDs are always offset by 10 hue steps apart.
**Challenge:** increase the offset between the two LEDs to 128 (opposite colors on the wheel).
---
### step4 — Display: text
**Goal:** initialize the ST7789 display and render text.
The display talks over SPI and needs explicit pin configuration on the nice!nano. After configuration, the drawable area is **240 × 135 pixels** in landscape orientation.
"Hello" and "Gophers!" appear on the display in yellow and magenta.
**Key concepts**
-`RowOffset` / `ColumnOffset` — the ST7789 physical memory does not always start at (0,0); these offsets align the driver to the actual pixel grid of this display module.
-`tinyfont.WriteLine(&display, &font, x, y, text, color)` — `x`, `y` are the **baseline** position of the text (not the top-left corner).
**Challenge:** change the font to `freesans.Regular9pt7b` and add a third line.
---
### step5 — Display + buttons
**Goal:** update the display in real time based on button state.
Three filled circles represent the three buttons. When a button is pressed a ring appears around its circle.
```sh
tinygo flash -target nicenano ./basics/step5
```
- Press **B** (left circle), **rotary** (center), or **A** (right circle) to see the corresponding ring.
**Key concepts**
-`tinydraw.FilledCircle` / `tinydraw.Circle` — drawing primitives from `tinygo.org/x/tinydraw`.
- Re-drawing a shape in the background color erases it — no need to clear the whole screen.
---
### step6 — Analog joystick
**Goal:** read the joystick's two analog axes and visualize the position on the display.
The joystick outputs a voltage proportional to its position on each axis. The ADC converts that voltage to a 16-bit integer (0–65535). The center resting position is approximately 32767.
```go
machine.InitADC()
ax := machine.ADC{Pin: machine.P0_02} // X axis
ay := machine.ADC{Pin: machine.P0_29} // Y axis
ax.Configure(machine.ADCConfig{})
ay.Configure(machine.ADCConfig{})
// map 0-65535 → display width/height
dotX := int16(uint32(ax.Get()) * 240 / 65535)
dotY := int16(uint32(ay.Get()) * 135 / 65535)
```
```sh
tinygo flash -target nicenano ./basics/step6
```
A green dot follows the joystick across the display.
**Key concepts**
-`machine.InitADC()` — must be called once before using any ADC pin.
- The cast chain `uint32(ax.Get()) * 240 / 65535` avoids integer overflow during the mapping (a plain `int16` multiplication would overflow).
---
### step7 — Rotary encoder
**Goal:** use the quadrature encoder to cycle through hue values on the LEDs.
A rotary encoder outputs two square waves (A and B) 90° out of phase. The `encoders` driver decodes the direction and accumulates a signed position value using interrupts.
- Turn the encoder knob → LED hue shifts smoothly.
- Press the encoder knob → resets position to zero (both LEDs snap back to the same starting hue).
**Key concepts**
-`Precision: 4` — number of encoder pulses per detent; adjust if your encoder feels too coarse or too fine.
-`enc.Position()` returns a signed `int32`; casting to `uint8` wraps around naturally (256 → 0), which is exactly what we want for the hue wheel.
---
### step8 — Buzzer
**Goal:** generate audio tones by toggling the buzzer pin at audio frequencies.
The buzzer is passive: it only makes sound when driven with an alternating signal. We create a tone by toggling the pin HIGH/LOW at the target frequency.
The half-period in microseconds equals `1_000_000 / (2 * freq_Hz)`, but here `freq` is passed directly as the half-period value in microseconds, so `freq=1046` means a half-period of 1046 µs ≈ 478 Hz. Use this table to pick a value:
| Note | Approx. half-period (µs) |
|------|--------------------------|
| C5 | 523 |
| F#5 | 739 |
| C6 | 1046 |
```sh
tinygo flash -target nicenano ./basics/step8
```
Each button plays a different note while held.
**Challenge:** compose a short melody by chaining `tone()` calls with `time.Sleep` pauses between them.
Open any online MIDI player (e.g. [muted.io/piano](https://muted.io/piano/)) or connect to a DAW. The three buttons play C4, E4, and G4 (a C major triad).
**Key concepts**
- MIDI NoteOn/NoteOff must be paired: always send NoteOff for the previous note before sending a new NoteOn, otherwise notes get stuck.
- Velocity (last parameter, `50`) controls how hard the note is "hit" (0–127).
**Challenge:** map the rotary encoder position to an octave shift (transpose all notes up or down by 12 semitones per detent).
**Goal:** use the joystick as a mouse pointer and buttons as mouse clicks.
The ADC center resting position (~32767) produces a raw offset of 0. A dead zone filters out the natural jitter around center so the cursor stays still when you're not touching the stick.
Connect the badge to a computer. The joystick moves the mouse cursor; button A is left click, button B is right click.
**Key concepts**
- The dead zone prevents cursor drift when the joystick is at rest.
-`rawX / 2048` scales down the ±32767 range to ±16, giving a comfortable cursor speed. Decrease the divisor for faster movement.
---
## BLE
The nice!nano's nRF52840 chip has built-in Bluetooth Low Energy. These examples use the [`github.com/tinygo-org/bluetooth`](https://github.com/tinygo-org/bluetooth) library.
The counter increments every second. `txChar.Write()` sends a notification to any subscribed central. If `Write` returns an error no device is listening — that is how connection state is tracked.
1. Flash the badge. The display shows `BLE: Advertising...`.
2. Open **nRF Toolbox** → UART → Connect → search for `NiceBadge`.
3. Once connected the display shows `BLE: Connected` and the counter appears in the terminal.
4. Type `reset` and send it — the counter resets to zero.
**Key concepts**
-`adapter.Enable()` — starts the BLE stack (SoftDevice on nRF52840). Must be called before anything else.
-`adapter.AddService` — registers the GATT service and its characteristics.
-`adv.Start()` — begins advertising; the badge is now discoverable.
- The `WriteEvent` callback runs when the central writes to a characteristic. It runs in a BLE interrupt context — keep it short.
---
### BLE step2 — LED color control
**Goal:** receive data from a mobile app and use it to set the LED color.
A custom service exposes a single writable characteristic. The central writes 3 bytes `[R, G, B]`; the badge lights both WS2812 LEDs immediately and shows the color + RGB values on the display.
After all services are registered the main goroutine simply blocks with `select {}` — all activity is driven by the BLE callback.
```sh
tinygo flash -target nicenano ./ble/step2
```
**How to test**
1. Flash and open **nRF Connect** (or LightBlue).
2. Connect to `NiceBadge` and expand the custom service (`BADA5501…`).
3. Write to the color characteristic. In nRF Connect, enter raw bytes in hex: `FF0000` = red, `00FF00` = green, `0000FF` = blue, `FF0080` = pink.
4. The LEDs and display update instantly.
**Key concepts**
- Custom 128-bit UUIDs let you define entirely private services not shared with any standard profile.
-`select {}` is idiomatic Go for blocking forever; it is more explicit than `for {}` with a sleep.
- Always validate `len(value)` in `WriteEvent` — a malformed write should not panic.
---
### BLE step3 — Scanner
**Goal:** put the radio in observer mode and display nearby BLE devices.
In scanner mode the badge does not advertise — it only listens. `adapter.Scan` is a blocking call, so it runs in a goroutine while the main loop updates the display every 500 ms.
```go
go func() {
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
name := result.LocalName()
if name == "" {
name = result.Address.String()
}
// deduplicate by address, update RSSI
})
}()
```
RSSI (Received Signal Strength Indicator) is expressed in dBm — closer to 0 is stronger. The display colors devices by signal quality:
| RSSI | Color | Meaning |
|------|-------|---------|
| > −60 dBm | green | Strong (< ~3 m) |
| −60 to −80 dBm | yellow | Medium |
| <−80 dBm | white | Weak |
```sh
tinygo flash -target nicenano ./ble/step3
```
Up to 5 nearby devices are listed by name and signal strength. Press **button A** to clear the list and start fresh.
**Key concepts**
-`result.LocalName()` returns the advertised name, if any. Devices that don't advertise a name are identified by their MAC address.
- TinyGo uses a cooperative scheduler — goroutines yield at blocking calls (`Scan`, `Sleep`, channel ops). Shared variables accessed from both the goroutine and the main loop are safe here because the scheduler is cooperative, but in general you should use channels or atomics.
---
## Next steps
- **Snake game tutorial** — coming soon.
- **Examples** (thermal camera, CO2 sensor, rubber duck) — see [`tutorial/examples/`](examples/).
- Combine what you learned: use the rotary encoder to scroll through a BLE device list, or send joystick data over NUS to a web app.