diff --git a/README.md b/README.md index 51c50b3..f1153a3 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,13 @@ The microcontroller is the **[nice!nano](https://nicekeyboards.com/nice-nano/)** ``` nicebadge/ -├── hardware/ # KiCad PCB design files +├── hardware/ # KiCad PCB design files │ └── PCB-kicad/ -│ └── production/ # Gerbers, BOM, pick-and-place files -└── tutorials/ # Step-by-step examples (coming soon) +│ └── production/ # Gerbers, BOM, pick-and-place files +└── tutorial/ # TinyGo tutorials and examples + ├── basics/ # Step-by-step basics (step0 → step10) + ├── ble/ # Bluetooth Low Energy examples + └── examples/ # Standalone examples (sensors, HID, etc.) ``` --- @@ -94,17 +97,59 @@ Production files (Gerbers, BOM, positions) ready for fabrication are in [hardwar ## Tutorials & examples -The tutorial series walks you through every peripheral of the badge, starting from blinking an LED and ending with a Bluetooth HID device: +All code lives under [`tutorial/`](tutorial/). Flash any step from inside that directory: -1. Hello, Badge! — blink the RGB LEDs -2. Drawing on the display — TinyGo + TinyDraw -3. Reading buttons and the joystick -4. Playing sounds with the buzzer -5. I2C expansion via StemmQT -6. Going wireless — BLE advertisements and GATT services -7. USB HID — turn the badge into a keyboard +```sh +cd tutorial +tinygo flash -target nicenano ./basics/step0 +``` -*(Tutorials are coming soon — contributions welcome!)* +Run `go mod tidy` once inside `tutorial/` to fetch all dependencies. + +--- + +### Basics + +A progressive series that introduces every peripheral one at a time. + +| Step | What it does | +|------|--------------| +| [step0](tutorial/basics/step0/) | Blink the built-in LED — verifies the flash toolchain works | +| [step1](tutorial/basics/step1/) | Built-in LED controlled by button A | +| [step2](tutorial/basics/step2/) | WS2812 RGB LEDs alternating red and green | +| [step3](tutorial/basics/step3/) | WS2812 LEDs change color with buttons A, B, and rotary | +| [step3b](tutorial/basics/step3b/) | Rainbow cycle on the LEDs, A/B scroll through the hue | +| [step4](tutorial/basics/step4/) | "Hello Gophers!" text on the display | +| [step5](tutorial/basics/step5/) | Display shows a circle per button; rings appear on press | +| [step6](tutorial/basics/step6/) | Analog joystick — a dot follows the stick on the display | +| [step7](tutorial/basics/step7/) | Rotary encoder — turning cycles LED colors, push resets | +| [step8](tutorial/basics/step8/) | Passive buzzer plays a note per button press | +| [step9](tutorial/basics/step9/) | USB MIDI — badge sends notes C4 / E4 / G4 to any DAW | +| [step10](tutorial/basics/step10/) | USB HID mouse — joystick moves the cursor, A/B click | + +--- + +### BLE + +Bluetooth Low Energy examples using the nRF52840 on the nice!nano. Compatible apps: **nRF Connect**, **nRF Toolbox**, **Serial Bluetooth Terminal**, **LightBlue**. + +| Step | What it does | +|------|--------------| +| [step1](tutorial/ble/step1/) | **Counter** — advertises as Nordic UART Service (NUS), sends an incrementing counter via BLE notifications every second; display shows connection status and current value; send `reset` from the app to restart the count | +| [step2](tutorial/ble/step2/) | **LED color control** — mobile writes 3 bytes (R, G, B) to a custom characteristic; both WS2812 LEDs and the display update immediately | +| [step3](tutorial/ble/step3/) | **Scanner** — lists nearby BLE devices with name and RSSI, color-coded by signal strength; button A clears the list | + +--- + +### Examples + +Standalone, feature-complete programs. + +| Example | What it does | Extra hardware | +|---------|--------------|----------------| +| [thermal-camera](tutorial/examples/thermal-camera/) | AMG88xx 8×8 IR sensor upscaled with bilinear interpolation and rendered with an iron-palette color map on the full display | AMG88xx sensor on I2C1 (SDA=P0_17, SCL=P0_20) | +| [rubber-duck](tutorial/examples/rubber-duck/) | USB HID keyboard automation (Ubuntu/GNOME target): opens a text editor, types a message, and launches a URL. Press button A to trigger | — | +| [co2-sensor](tutorial/examples/co2-sensor/) | SCD4x CO2/temperature/humidity sensor: display background turns green/yellow/red by CO2 level, LEDs mirror the color, buzzer warns above 1500 ppm | SCD4x sensor on I2C1 (SDA=P0_17, SCL=P0_20) | --- diff --git a/examples/co2-sensor/main.go b/examples/co2-sensor/main.go new file mode 100644 index 0000000..87e9cef --- /dev/null +++ b/examples/co2-sensor/main.go @@ -0,0 +1,169 @@ +package main + +// CO2 sensor example using SCD4x (I2C). +// Connect the sensor to I2C1: SDA=P0_17, SCL=P0_20 (3.3V power). +// +// The display background changes color based on CO2 level: +// green → < 800 ppm (good air quality) +// yellow → < 1500 ppm (ventilate soon) +// red → ≥ 1500 ppm (ventilate now!) +// +// The WS2812 LEDs mirror the same color, and the buzzer sounds a +// warning tone when CO2 exceeds the danger threshold. + +import ( + "image/color" + "machine" + "strconv" + "time" + + "tinygo.org/x/drivers/scd4x" + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/drivers/ws2812" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freesans" +) + +var ( + display st7789.Device + leds ws2812.Device + bzrPin machine.Pin + co2dev *scd4x.Device +) + +func main() { + time.Sleep(3 * time.Second) + + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + + machine.I2C1.Configure(machine.I2CConfig{ + SDA: machine.P0_17, + SCL: machine.P0_20, + Frequency: 400000, + }) + + display = st7789.New(machine.SPI0, + machine.P1_15, // TFT_RESET + machine.P1_13, // TFT_DC + machine.P0_10, // TFT_CS + machine.P0_09) // TFT_LITE + + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + + neo := machine.P1_11 + neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) + leds = ws2812.New(neo) + + bzrPin = machine.P0_31 + bzrPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + co2dev = scd4x.New(machine.I2C1) + co2dev.Configure() + + display.FillScreen(color.RGBA{0, 0, 0, 255}) + + if err := co2dev.StartPeriodicMeasurement(); err != nil { + println(err) + } + + var ( + co2 int32 + temp int32 + hum int32 + err error + bg color.RGBA + oldbg = color.RGBA{R: 0xff, G: 0xff, B: 0xff} + black = color.RGBA{0, 0, 0, 255} + ) + + for { + // SCD4x needs up to 5s between measurements; retry a few times + for i := 0; i < 5; i++ { + co2, err = co2dev.ReadCO2() + temp, _ = co2dev.ReadTemperature() + hum, _ = co2dev.ReadHumidity() + if err != nil { + println(err) + } + if co2 != 0 { + break + } + time.Sleep(200 * time.Millisecond) + } + + switch { + case co2 < 800: + bg = color.RGBA{0, 220, 0, 255} + case co2 < 1500: + bg = color.RGBA{220, 220, 0, 255} + default: + bg = color.RGBA{220, 0, 0, 255} + } + + if bg != oldbg { + display.FillScreen(bg) + oldbg = bg + } + + // mirror CO2 status on the LEDs + leds.WriteColors([]color.RGBA{bg, bg}) + + // buzzer warning when CO2 is dangerously high + if co2 >= 1500 { + warn() + } + + // display 240x135, text layout: + // y=40 CO2 level (most prominent) + // y=75 Temperature + // y=110 Humidity + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 40, "CO2: "+strconv.Itoa(int(co2))+" ppm", black) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 75, "Temp: "+formatMilliC(temp), black) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, "Hum: "+formatMilliPct(hum), black) + + time.Sleep(time.Second) + + // erase text by redrawing in background color (avoids full FillScreen flicker) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 40, "CO2: "+strconv.Itoa(int(co2))+" ppm", bg) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 75, "Temp: "+formatMilliC(temp), bg) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, "Hum: "+formatMilliPct(hum), bg) + } +} + +// formatMilliC formats a millidegree Celsius value as "XX.X C". +func formatMilliC(mc int32) string { + if mc < 0 { + mc = -mc + return "-" + strconv.Itoa(int(mc/1000)) + "." + strconv.Itoa(int((mc%1000)/100)) + " C" + } + return strconv.Itoa(int(mc/1000)) + "." + strconv.Itoa(int((mc%1000)/100)) + " C" +} + +// formatMilliPct formats a millipercent value as "XX.X %". +func formatMilliPct(mp int32) string { + return strconv.Itoa(int(mp/1000)) + "." + strconv.Itoa(int((mp%1000)/100)) + " %" +} + +// warn sounds two short beeps on the buzzer. +func warn() { + for beep := 0; beep < 2; beep++ { + for i := 0; i < 100; i++ { + bzrPin.High() + time.Sleep(500 * time.Microsecond) + bzrPin.Low() + time.Sleep(500 * time.Microsecond) + } + time.Sleep(200 * time.Millisecond) + } +} diff --git a/examples/rubber-duck/main.go b/examples/rubber-duck/main.go new file mode 100644 index 0000000..ca57969 --- /dev/null +++ b/examples/rubber-duck/main.go @@ -0,0 +1,83 @@ +package main + +// Rubber Duck attack - USB HID keyboard automation. +// The badge presents itself as a USB keyboard and executes a sequence of +// keystrokes on the connected computer. +// +// TARGETS UBUNTU/GNOME. For other platforms adjust the key sequences. +// NOTE: keyboard scan codes are US layout; non-US layouts may produce +// different characters for symbols (/, -, :, etc.). +// +// Press button A to trigger the attack. The badge waits on startup +// so you have time to plug it in safely. + +import ( + "machine" + "machine/usb/hid/keyboard" + "time" +) + +func main() { + // wait for USB enumeration + time.Sleep(2 * time.Second) + + // button A arms the attack — only runs when pressed + btnA := machine.P1_06 + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + kb := keyboard.Port() + + for { + if !btnA.Get() { + runAttack(kb) + // only run once per press + for !btnA.Get() { + time.Sleep(50 * time.Millisecond) + } + } + time.Sleep(10 * time.Millisecond) + } +} + +func runAttack(kb keyboard.Device) { + // open application launcher (Super key on Ubuntu/GNOME) + kb.Down(keyboard.KeyLeftGUI) + time.Sleep(time.Second) + kb.Up(keyboard.KeyLeftGUI) + time.Sleep(500 * time.Millisecond) + + // search for text editor + kb.Write([]byte("text")) + time.Sleep(1500 * time.Millisecond) + kb.Press(keyboard.KeyEnter) + time.Sleep(time.Second) + + // type the ominous message + kb.Write([]byte("Please wait while you are being hacked")) + time.Sleep(2 * time.Second) + + // open run dialog (Alt+F2 on GNOME) and launch xdg-open + // NOTE: symbols below assume US keyboard layout + kb.Down(keyboard.KeyLeftAlt) + kb.Down(keyboard.KeyF2) + time.Sleep(time.Second) + kb.Up(keyboard.KeyF2) + kb.Up(keyboard.KeyLeftAlt) + time.Sleep(time.Second) + + kb.Write([]byte("xdg")) + kb.Press(keyboard.KeypadMinus) + kb.Write([]byte("open https>")) + kb.Press(keyboard.KeypadSlash) + kb.Press(keyboard.KeypadSlash) + kb.Write([]byte("www.youtube.com")) + kb.Press(keyboard.KeypadSlash) + kb.Write([]byte("watch_v)dQw4w9WgXcQ")) + kb.Press(keyboard.KeyEnter) + + // turn the volume up + time.Sleep(500 * time.Millisecond) + for i := 0; i < 12; i++ { + kb.Press(keyboard.KeyMediaVolumeInc) + } +} diff --git a/examples/thermal-camera/colors.go b/examples/thermal-camera/colors.go new file mode 100644 index 0000000..1a228aa --- /dev/null +++ b/examples/thermal-camera/colors.go @@ -0,0 +1,6 @@ +package main + +import "image/color" + +// palette of colors (iron) taken from http://ramphastosramblings.blogspot.com/2014/03/choosing-appropriate-colour-palette-for.html +var colors = []color.RGBA{{0, 0, 10, 255}, {0, 0, 20, 255}, {0, 0, 30, 255}, {0, 0, 37, 255}, {0, 0, 42, 255}, {0, 0, 46, 255}, {0, 0, 50, 255}, {0, 0, 54, 255}, {0, 0, 58, 255}, {0, 0, 62, 255}, {0, 0, 66, 255}, {0, 0, 70, 255}, {0, 0, 74, 255}, {0, 0, 79, 255}, {0, 0, 82, 255}, {1, 0, 85, 255}, {1, 0, 87, 255}, {2, 0, 89, 255}, {2, 0, 92, 255}, {3, 0, 94, 255}, {4, 0, 97, 255}, {4, 0, 99, 255}, {5, 0, 101, 255}, {6, 0, 103, 255}, {7, 0, 105, 255}, {8, 0, 107, 255}, {9, 0, 110, 255}, {10, 0, 112, 255}, {11, 0, 115, 255}, {12, 0, 116, 255}, {13, 0, 117, 255}, {13, 0, 118, 255}, {14, 0, 119, 255}, {16, 0, 120, 255}, {18, 0, 121, 255}, {19, 0, 123, 255}, {21, 0, 124, 255}, {23, 0, 125, 255}, {25, 0, 126, 255}, {27, 0, 128, 255}, {28, 0, 129, 255}, {30, 0, 131, 255}, {32, 0, 132, 255}, {34, 0, 133, 255}, {36, 0, 134, 255}, {38, 0, 135, 255}, {40, 0, 137, 255}, {42, 0, 137, 255}, {44, 0, 138, 255}, {46, 0, 139, 255}, {48, 0, 140, 255}, {50, 0, 141, 255}, {52, 0, 142, 255}, {54, 0, 142, 255}, {56, 0, 143, 255}, {57, 0, 144, 255}, {59, 0, 145, 255}, {60, 0, 146, 255}, {62, 0, 147, 255}, {63, 0, 147, 255}, {65, 0, 148, 255}, {66, 0, 149, 255}, {68, 0, 149, 255}, {69, 0, 150, 255}, {71, 0, 150, 255}, {73, 0, 150, 255}, {74, 0, 150, 255}, {76, 0, 151, 255}, {78, 0, 151, 255}, {79, 0, 151, 255}, {81, 0, 151, 255}, {82, 0, 152, 255}, {84, 0, 152, 255}, {86, 0, 152, 255}, {88, 0, 153, 255}, {90, 0, 153, 255}, {92, 0, 153, 255}, {93, 0, 154, 255}, {95, 0, 154, 255}, {97, 0, 155, 255}, {99, 0, 155, 255}, {100, 0, 155, 255}, {102, 0, 155, 255}, {104, 0, 155, 255}, {106, 0, 155, 255}, {108, 0, 156, 255}, {109, 0, 156, 255}, {111, 0, 156, 255}, {112, 0, 156, 255}, {113, 0, 157, 255}, {115, 0, 157, 255}, {117, 0, 157, 255}, {119, 0, 157, 255}, {120, 0, 157, 255}, {122, 0, 157, 255}, {124, 0, 157, 255}, {126, 0, 157, 255}, {127, 0, 157, 255}, {129, 0, 157, 255}, {131, 0, 157, 255}, {132, 0, 157, 255}, {134, 0, 157, 255}, {135, 0, 157, 255}, {137, 0, 157, 255}, {138, 0, 157, 255}, {139, 0, 157, 255}, {141, 0, 157, 255}, {143, 0, 156, 255}, {145, 0, 156, 255}, {147, 0, 156, 255}, {149, 0, 156, 255}, {150, 0, 155, 255}, {152, 0, 155, 255}, {153, 0, 155, 255}, {155, 0, 155, 255}, {156, 0, 155, 255}, {157, 0, 155, 255}, {159, 0, 155, 255}, {160, 0, 155, 255}, {162, 0, 155, 255}, {163, 0, 155, 255}, {164, 0, 155, 255}, {166, 0, 154, 255}, {167, 0, 154, 255}, {168, 0, 154, 255}, {169, 0, 153, 255}, {170, 0, 153, 255}, {171, 0, 153, 255}, {173, 0, 153, 255}, {174, 1, 152, 255}, {175, 1, 152, 255}, {176, 1, 152, 255}, {176, 1, 152, 255}, {177, 1, 151, 255}, {178, 1, 151, 255}, {179, 1, 150, 255}, {180, 2, 150, 255}, {181, 2, 149, 255}, {182, 2, 149, 255}, {183, 3, 149, 255}, {184, 3, 149, 255}, {185, 4, 149, 255}, {186, 4, 149, 255}, {186, 4, 148, 255}, {187, 5, 147, 255}, {188, 5, 147, 255}, {189, 5, 147, 255}, {190, 6, 146, 255}, {191, 6, 146, 255}, {191, 6, 146, 255}, {192, 7, 145, 255}, {192, 7, 145, 255}, {193, 8, 144, 255}, {193, 9, 144, 255}, {194, 10, 143, 255}, {195, 10, 142, 255}, {195, 11, 142, 255}, {196, 12, 141, 255}, {197, 12, 140, 255}, {198, 13, 139, 255}, {198, 14, 138, 255}, {199, 15, 137, 255}, {200, 16, 136, 255}, {201, 17, 135, 255}, {202, 18, 134, 255}, {202, 19, 133, 255}, {203, 19, 133, 255}, {203, 20, 132, 255}, {204, 21, 130, 255}, {205, 22, 129, 255}, {206, 23, 128, 255}, {206, 24, 126, 255}, {207, 24, 124, 255}, {207, 25, 123, 255}, {208, 26, 121, 255}, {209, 27, 120, 255}, {209, 28, 118, 255}, {210, 28, 117, 255}, {210, 29, 116, 255}, {211, 30, 114, 255}, {211, 32, 113, 255}, {212, 33, 111, 255}, {212, 34, 110, 255}, {213, 35, 107, 255}, {213, 36, 105, 255}, {214, 37, 103, 255}, {215, 38, 101, 255}, {216, 39, 100, 255}, {216, 40, 98, 255}, {217, 42, 96, 255}, {218, 43, 94, 255}, {218, 44, 92, 255}, {219, 46, 90, 255}, {219, 47, 87, 255}, {220, 47, 84, 255}, {221, 48, 81, 255}, {221, 49, 78, 255}, {222, 50, 74, 255}, {222, 51, 71, 255}, {223, 52, 68, 255}, {223, 53, 65, 255}, {223, 54, 61, 255}, {224, 55, 58, 255}, {224, 56, 55, 255}, {224, 57, 51, 255}, {225, 58, 48, 255}, {226, 59, 45, 255}, {226, 60, 42, 255}, {227, 61, 38, 255}, {227, 62, 35, 255}, {228, 63, 32, 255}, {228, 65, 29, 255}, {228, 66, 28, 255}, {229, 67, 27, 255}, {229, 68, 25, 255}, {229, 69, 24, 255}, {230, 70, 22, 255}, {231, 71, 21, 255}, {231, 72, 20, 255}, {231, 73, 19, 255}, {232, 74, 18, 255}, {232, 76, 16, 255}, {232, 76, 15, 255}, {233, 77, 14, 255}, {233, 77, 13, 255}, {234, 78, 12, 255}, {234, 79, 12, 255}, {235, 80, 11, 255}, {235, 81, 10, 255}, {235, 82, 10, 255}, {235, 83, 9, 255}, {236, 84, 9, 255}, {236, 86, 8, 255}, {236, 87, 8, 255}, {236, 88, 8, 255}, {237, 89, 7, 255}, {237, 90, 7, 255}, {237, 91, 6, 255}, {238, 92, 6, 255}, {238, 92, 5, 255}, {238, 93, 5, 255}, {238, 94, 5, 255}, {239, 95, 4, 255}, {239, 96, 4, 255}, {239, 97, 4, 255}, {239, 98, 4, 255}, {240, 99, 3, 255}, {240, 100, 3, 255}, {240, 101, 3, 255}, {241, 102, 3, 255}, {241, 102, 3, 255}, {241, 103, 3, 255}, {241, 104, 3, 255}, {241, 105, 2, 255}, {241, 106, 2, 255}, {241, 107, 2, 255}, {241, 107, 2, 255}, {242, 108, 1, 255}, {242, 109, 1, 255}, {242, 110, 1, 255}, {243, 111, 1, 255}, {243, 112, 1, 255}, {243, 113, 1, 255}, {243, 114, 1, 255}, {244, 115, 0, 255}, {244, 116, 0, 255}, {244, 117, 0, 255}, {244, 118, 0, 255}, {244, 119, 0, 255}, {244, 120, 0, 255}, {244, 122, 0, 255}, {245, 123, 0, 255}, {245, 124, 0, 255}, {245, 126, 0, 255}, {245, 127, 0, 255}, {246, 128, 0, 255}, {246, 129, 0, 255}, {246, 130, 0, 255}, {247, 131, 0, 255}, {247, 132, 0, 255}, {247, 133, 0, 255}, {247, 134, 0, 255}, {248, 135, 0, 255}, {248, 136, 0, 255}, {248, 136, 0, 255}, {248, 137, 0, 255}, {248, 138, 0, 255}, {248, 139, 0, 255}, {248, 140, 0, 255}, {249, 141, 0, 255}, {249, 141, 0, 255}, {249, 142, 0, 255}, {249, 143, 0, 255}, {249, 144, 0, 255}, {249, 145, 0, 255}, {249, 146, 0, 255}, {249, 147, 0, 255}, {250, 148, 0, 255}, {250, 149, 0, 255}, {250, 150, 0, 255}, {251, 152, 0, 255}, {251, 153, 0, 255}, {251, 154, 0, 255}, {251, 156, 0, 255}, {252, 157, 0, 255}, {252, 159, 0, 255}, {252, 160, 0, 255}, {252, 161, 0, 255}, {253, 162, 0, 255}, {253, 163, 0, 255}, {253, 164, 0, 255}, {253, 166, 0, 255}, {253, 167, 0, 255}, {253, 168, 0, 255}, {253, 170, 0, 255}, {253, 171, 0, 255}, {253, 172, 0, 255}, {253, 173, 0, 255}, {253, 174, 0, 255}, {254, 175, 0, 255}, {254, 176, 0, 255}, {254, 177, 0, 255}, {254, 178, 0, 255}, {254, 179, 0, 255}, {254, 180, 0, 255}, {254, 181, 0, 255}, {254, 182, 0, 255}, {254, 184, 0, 255}, {254, 185, 0, 255}, {254, 185, 0, 255}, {254, 186, 0, 255}, {254, 187, 0, 255}, {254, 188, 0, 255}, {254, 189, 0, 255}, {254, 190, 0, 255}, {254, 192, 0, 255}, {254, 193, 0, 255}, {254, 194, 0, 255}, {254, 195, 0, 255}, {254, 196, 0, 255}, {254, 197, 0, 255}, {254, 198, 0, 255}, {254, 199, 0, 255}, {254, 200, 0, 255}, {254, 201, 1, 255}, {254, 202, 1, 255}, {254, 202, 1, 255}, {254, 203, 1, 255}, {254, 204, 2, 255}, {254, 205, 2, 255}, {254, 206, 3, 255}, {254, 207, 4, 255}, {254, 207, 4, 255}, {254, 208, 5, 255}, {254, 209, 6, 255}, {254, 211, 8, 255}, {254, 212, 9, 255}, {254, 213, 10, 255}, {254, 214, 10, 255}, {254, 215, 11, 255}, {254, 216, 12, 255}, {254, 217, 13, 255}, {255, 218, 14, 255}, {255, 218, 14, 255}, {255, 219, 16, 255}, {255, 220, 18, 255}, {255, 220, 20, 255}, {255, 221, 22, 255}, {255, 222, 25, 255}, {255, 222, 27, 255}, {255, 223, 30, 255}, {255, 224, 32, 255}, {255, 225, 34, 255}, {255, 226, 36, 255}, {255, 226, 38, 255}, {255, 227, 40, 255}, {255, 228, 43, 255}, {255, 228, 46, 255}, {255, 229, 49, 255}, {255, 230, 53, 255}, {255, 230, 56, 255}, {255, 231, 60, 255}, {255, 232, 63, 255}, {255, 233, 67, 255}, {255, 234, 70, 255}, {255, 235, 73, 255}, {255, 235, 77, 255}, {255, 236, 80, 255}, {255, 237, 84, 255}, {255, 238, 87, 255}, {255, 238, 91, 255}, {255, 238, 95, 255}, {255, 239, 99, 255}, {255, 239, 103, 255}, {255, 240, 106, 255}, {255, 240, 110, 255}, {255, 241, 114, 255}, {255, 241, 119, 255}, {255, 241, 123, 255}, {255, 242, 128, 255}, {255, 242, 133, 255}, {255, 242, 138, 255}, {255, 243, 142, 255}, {255, 244, 146, 255}, {255, 244, 150, 255}, {255, 244, 154, 255}, {255, 245, 158, 255}, {255, 245, 162, 255}, {255, 245, 166, 255}, {255, 246, 170, 255}, {255, 246, 175, 255}, {255, 247, 179, 255}, {255, 247, 182, 255}, {255, 248, 186, 255}, {255, 248, 189, 255}, {255, 248, 193, 255}, {255, 248, 196, 255}, {255, 249, 199, 255}, {255, 249, 202, 255}, {255, 249, 205, 255}, {255, 250, 209, 255}, {255, 250, 212, 255}, {255, 251, 216, 255}, {255, 252, 219, 255}, {255, 252, 223, 255}, {255, 253, 226, 255}, {255, 253, 229, 255}, {255, 253, 232, 255}, {255, 254, 235, 255}, {255, 254, 238, 255}, {255, 254, 241, 255}, {255, 254, 244, 255}, {255, 255, 246, 255}} diff --git a/examples/thermal-camera/main.go b/examples/thermal-camera/main.go new file mode 100644 index 0000000..4f64e71 --- /dev/null +++ b/examples/thermal-camera/main.go @@ -0,0 +1,102 @@ +package main + +// Thermal camera example using AMG88xx 8x8 IR sensor. +// Connect the sensor to I2C1: SDA=P0_17, SCL=P0_20 (3.3V power). +// +// The 8x8 sensor data is scaled to 24x24 with bilinear interpolation and +// rendered with an iron-palette color map on the 240x135 display. +// Each scaled pixel is drawn as a 10x5 px block → 240x120 total, centered. + +import ( + "image" + "machine" + + draw2 "golang.org/x/image/draw" + + "tinygo.org/x/drivers/amg88xx" + "tinygo.org/x/drivers/st7789" +) + +const ( + sensorSize = 8 // AMG88xx is 8x8 + scaledSize = 24 // intermediate bilinear-scaled image + blockW = 10 // px per scaled pixel (horizontal): 24*10 = 240 + blockH = 5 // px per scaled pixel (vertical): 24*5 = 120 + yOffset = 7 // center 120px vertically in 135px display: (135-120)/2 +) + +var ( + display st7789.Device + data [sensorSize * sensorSize]int16 +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + + machine.I2C1.Configure(machine.I2CConfig{ + SDA: machine.P0_17, + SCL: machine.P0_20, + Frequency: 400000, + }) + + display = st7789.New(machine.SPI0, + machine.P1_15, // TFT_RESET + machine.P1_13, // TFT_DC + machine.P0_10, // TFT_CS + machine.P0_09) // TFT_LITE + + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + + camera := amg88xx.New(machine.I2C1) + camera.Configure(amg88xx.Config{}) + + src := image.NewRGBA(image.Rect(0, 0, sensorSize, sensorSize)) + dst := image.NewRGBA(image.Rect(0, 0, scaledSize, scaledSize)) + + for { + camera.ReadPixels(&data) + + // map each sensor pixel to a palette color + for j := 0; j < sensorSize; j++ { + for i := 0; i < sensorSize; i++ { + v := data[63-(i+j*sensorSize)] + // clamp to 18°C–33°C range → index 0–432 + if v < 18000 { + v = 0 + } else { + v = (v - 18000) / 36 + if v > 432 { + v = 432 + } + } + src.Set(i, j, colors[v]) + } + } + + // bilinear upscale 8x8 → 24x24 + draw2.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw2.Over, nil) + + // draw horizontally mirrored (acts as a selfie mirror) + for j := 0; j < scaledSize; j++ { + for i := 0; i < scaledSize; i++ { + display.FillRectangle( + int16((scaledSize-1-i)*blockW), + yOffset+int16(j)*blockH, + blockW, blockH, + dst.RGBAAt(i, j), + ) + } + } + } +} diff --git a/tutorial/basics/step0/main.go b/tutorial/basics/step0/main.go new file mode 100644 index 0000000..5220583 --- /dev/null +++ b/tutorial/basics/step0/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "machine" + "time" +) + +func main() { + + // get the built-in LED pin on the nice!nano board + led := machine.LED + + // configure the LED pin for output + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + for { + // turn off the LED + led.Low() + + // wait 500 ms + time.Sleep(time.Millisecond * 500) + + // turn on the LED + led.High() + + // wait 500 ms + time.Sleep(time.Millisecond * 500) + } +} diff --git a/tutorial/basics/step1/main.go b/tutorial/basics/step1/main.go new file mode 100644 index 0000000..56a048d --- /dev/null +++ b/tutorial/basics/step1/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "machine" + "time" +) + +func main() { + + led := machine.LED + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // button A is on pin P1_06, active LOW (internal pull-up) + btnA := machine.P1_06 + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + for { + // button reads LOW when pressed + if !btnA.Get() { + led.High() + } else { + led.Low() + } + + time.Sleep(time.Millisecond * 10) + } +} diff --git a/tutorial/basics/step10/main.go b/tutorial/basics/step10/main.go new file mode 100644 index 0000000..50ddeef --- /dev/null +++ b/tutorial/basics/step10/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "machine" + "machine/usb/hid/mouse" + "time" +) + +const DEADZONE = 5000 + +func main() { + + // joystick analog axes + 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{}) + + // A = left click, B = right click + btnA := machine.P1_06 + btnB := machine.P1_04 + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + mouseDevice := mouse.Port() + + for { + // ADC center is ~32767; compute offset from center + rawX := int(ax.Get()) - 32767 + rawY := int(ay.Get()) - 32767 + + var dx, dy int + if rawX > DEADZONE || rawX < -DEADZONE { + dx = rawX / 2048 + } + if rawY > DEADZONE || rawY < -DEADZONE { + dy = rawY / 2048 + } + + mouseDevice.Move(dx, dy) + + if !btnA.Get() { + mouseDevice.Click(mouse.Left) + } + if !btnB.Get() { + mouseDevice.Click(mouse.Right) + } + + time.Sleep(100 * time.Millisecond) + } +} diff --git a/tutorial/basics/step2/main.go b/tutorial/basics/step2/main.go new file mode 100644 index 0000000..4b4dcb3 --- /dev/null +++ b/tutorial/basics/step2/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/ws2812" +) + +const ( + NumberOfLEDs = 2 +) + +var ( + red = color.RGBA{255, 0, 0, 255} + green = color.RGBA{0, 255, 0, 255} +) + +func main() { + // WS2812 LEDs are on pin P1_11 + neo := machine.P1_11 + neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + leds := ws2812.New(neo) + ledColors := make([]color.RGBA, NumberOfLEDs) + + rg := false + for { + for i := 0; i < NumberOfLEDs; i++ { + if rg { + ledColors[i] = red + } else { + ledColors[i] = green + } + rg = !rg + } + leds.WriteColors(ledColors) + rg = !rg + time.Sleep(time.Millisecond * 300) + } +} diff --git a/tutorial/basics/step3/main.go b/tutorial/basics/step3/main.go new file mode 100644 index 0000000..1d53c99 --- /dev/null +++ b/tutorial/basics/step3/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/ws2812" +) + +const ( + Red = iota + Green + Blue + Yellow + Cyan + Purple + White + Off +) + +var colors = [...]color.RGBA{ + {255, 0, 0, 255}, // Red + {0, 255, 0, 255}, // Green + {0, 0, 255, 255}, // Blue + {255, 255, 0, 255}, // Yellow + {0, 255, 255, 255}, // Cyan + {255, 0, 255, 255}, // Purple + {255, 255, 255, 255}, // White + {0, 0, 0, 255}, // Off +} + +func main() { + + // WS2812 LEDs are on pin P1_11 + neo := machine.P1_11 + neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + leds := ws2812.New(neo) + ledColors := make([]color.RGBA, 2) + + // buttons: active LOW (internal pull-up) + btnA := machine.P1_06 + btnB := machine.P1_04 + btnRot := machine.P0_22 // rotary encoder push button + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + c := Off + for { + if !btnA.Get() { + c = Red + } + if !btnB.Get() { + c = Blue + } + if !btnRot.Get() { + c = Green + } + + for i := range ledColors { + ledColors[i] = colors[c] + } + + leds.WriteColors(ledColors) + time.Sleep(30 * time.Millisecond) + } +} diff --git a/tutorial/basics/step3b/main.go b/tutorial/basics/step3b/main.go new file mode 100644 index 0000000..aeee54a --- /dev/null +++ b/tutorial/basics/step3b/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/ws2812" +) + +func main() { + + // WS2812 LEDs are on pin P1_11 + neo := machine.P1_11 + neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + leds := ws2812.New(neo) + ledColors := make([]color.RGBA, 2) + + // A cycles forward, B cycles backward through the rainbow + btnA := machine.P1_06 + btnB := machine.P1_04 + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + var k uint8 + for { + if !btnA.Get() { + k++ + } + if !btnB.Get() { + k-- + } + + ledColors[0] = getRainbowRGB(k) + ledColors[1] = getRainbowRGB(k + 10) + leds.WriteColors(ledColors) + + time.Sleep(10 * time.Millisecond) + } +} + +func getRainbowRGB(i uint8) color.RGBA { + if i < 85 { + return color.RGBA{i * 3, 255 - i*3, 0, 255} + } else if i < 170 { + i -= 85 + return color.RGBA{255 - i*3, 0, i * 3, 255} + } + i -= 170 + return color.RGBA{0, i * 3, 255 - i*3, 255} +} diff --git a/tutorial/basics/step4/main.go b/tutorial/basics/step4/main.go new file mode 100644 index 0000000..4654d16 --- /dev/null +++ b/tutorial/basics/step4/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "image/color" + "machine" + + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freesans" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + + display := st7789.New(machine.SPI0, + machine.P1_15, // TFT_RESET + machine.P1_13, // TFT_DC + machine.P0_10, // TFT_CS + machine.P0_09) // TFT_LITE + + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + + // clear the screen to black + display.FillScreen(color.RGBA{0, 0, 0, 255}) + + // the display is 240x135 pixels in landscape orientation + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 50, "Hello", color.RGBA{R: 255, G: 255, B: 0, A: 255}) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 90, "Gophers!", color.RGBA{R: 255, G: 0, B: 255, A: 255}) +} diff --git a/tutorial/basics/step5/main.go b/tutorial/basics/step5/main.go new file mode 100644 index 0000000..afd69d7 --- /dev/null +++ b/tutorial/basics/step5/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/tinydraw" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + + display := st7789.New(machine.SPI0, + machine.P1_15, // TFT_RESET + machine.P1_13, // TFT_DC + machine.P0_10, // TFT_CS + machine.P0_09) // TFT_LITE + + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + + // buttons: active LOW (internal pull-up) + btnA := machine.P1_06 + btnB := machine.P1_04 + btnRot := machine.P0_22 // rotary encoder push button + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + white := color.RGBA{255, 255, 255, 255} + circle := color.RGBA{0, 100, 250, 255} + ring := color.RGBA{200, 0, 0, 255} + + display.FillScreen(white) + + // draw a circle for each button on the 240x135 display + tinydraw.FilledCircle(&display, 60, 67, 14, circle) // B (left) + tinydraw.FilledCircle(&display, 120, 67, 14, circle) // rotary button (center) + tinydraw.FilledCircle(&display, 180, 67, 14, circle) // A (right) + + for { + if !btnB.Get() { + tinydraw.Circle(&display, 60, 67, 16, ring) + } else { + tinydraw.Circle(&display, 60, 67, 16, white) + } + if !btnRot.Get() { + tinydraw.Circle(&display, 120, 67, 16, ring) + } else { + tinydraw.Circle(&display, 120, 67, 16, white) + } + if !btnA.Get() { + tinydraw.Circle(&display, 180, 67, 16, ring) + } else { + tinydraw.Circle(&display, 180, 67, 16, white) + } + + time.Sleep(50 * time.Millisecond) + } +} diff --git a/tutorial/basics/step6/main.go b/tutorial/basics/step6/main.go new file mode 100644 index 0000000..5bfef78 --- /dev/null +++ b/tutorial/basics/step6/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/tinydraw" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + + display := st7789.New(machine.SPI0, + machine.P1_15, // TFT_RESET + machine.P1_13, // TFT_DC + machine.P0_10, // TFT_CS + machine.P0_09) // TFT_LITE + + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + + // joystick analog axes + 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{}) + + black := color.RGBA{0, 0, 0, 255} + dot := color.RGBA{0, 255, 100, 255} + + display.FillScreen(black) + + var prevX, prevY int16 + for { + // ADC returns 0-65535; map to display dimensions (240x135) + dotX := int16(uint32(ax.Get()) * 240 / 65535) + dotY := int16(uint32(ay.Get()) * 135 / 65535) + + // erase previous dot + tinydraw.FilledCircle(&display, prevX, prevY, 5, black) + + // draw new dot + tinydraw.FilledCircle(&display, dotX, dotY, 5, dot) + + prevX = dotX + prevY = dotY + + time.Sleep(30 * time.Millisecond) + } +} diff --git a/tutorial/basics/step7/main.go b/tutorial/basics/step7/main.go new file mode 100644 index 0000000..cbdaf80 --- /dev/null +++ b/tutorial/basics/step7/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/encoders" + "tinygo.org/x/drivers/ws2812" +) + +func main() { + // WS2812 LEDs are on pin P1_11 + neo := machine.P1_11 + neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + leds := ws2812.New(neo) + ledColors := make([]color.RGBA, 2) + + // rotary encoder: A=P1_00, B=P0_24; push button=P0_22 + enc := encoders.NewQuadratureViaInterrupt(machine.P1_00, machine.P0_24) + enc.Configure(encoders.QuadratureConfig{Precision: 4}) + + btnRot := machine.P0_22 + btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + var k uint8 + for { + // pressing the rotary button resets position to zero + if !btnRot.Get() { + enc.SetPosition(0) + } + + // encoder position cycles through the rainbow + k = uint8(enc.Position()) + + ledColors[0] = getRainbowRGB(k) + ledColors[1] = getRainbowRGB(k + 85) + leds.WriteColors(ledColors) + + time.Sleep(20 * time.Millisecond) + } +} + +func getRainbowRGB(i uint8) color.RGBA { + if i < 85 { + return color.RGBA{i * 3, 255 - i*3, 0, 255} + } else if i < 170 { + i -= 85 + return color.RGBA{255 - i*3, 0, i * 3, 255} + } + i -= 170 + return color.RGBA{0, i * 3, 255 - i*3, 255} +} diff --git a/tutorial/basics/step8/main.go b/tutorial/basics/step8/main.go new file mode 100644 index 0000000..0e84c2b --- /dev/null +++ b/tutorial/basics/step8/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "machine" + "time" +) + +var bzrPin machine.Pin +var btnA, btnB, btnRot machine.Pin + +func main() { + + // buttons: active LOW (internal pull-up) + btnA = machine.P1_06 + btnB = machine.P1_04 + btnRot = machine.P0_22 // rotary encoder push button + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + // passive buzzer on P0_31 + bzrPin = machine.P0_31 + bzrPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + for { + if !btnA.Get() { + tone(1046) // C6 + } + if !btnB.Get() { + tone(739) // F#5 + } + if !btnRot.Get() { + tone(523) // C5 + } + } +} + +func tone(freq int) { + for i := 0; i < 10; i++ { + bzrPin.High() + time.Sleep(time.Duration(freq) * time.Microsecond) + + bzrPin.Low() + time.Sleep(time.Duration(freq) * time.Microsecond) + } +} diff --git a/tutorial/basics/step9/main.go b/tutorial/basics/step9/main.go new file mode 100644 index 0000000..64d873b --- /dev/null +++ b/tutorial/basics/step9/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "machine" + "machine/usb/adc/midi" + "time" +) + +func main() { + + // buttons: active LOW (internal pull-up) + btnA := machine.P1_06 + btnB := machine.P1_04 + btnRot := machine.P0_22 // rotary encoder push button + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + // C major triad: C4, E4, G4 + notes := []midi.Note{midi.C4, midi.E4, midi.G4} + midichannel := uint8(1) + + note := -1 + oldNote := -1 + for { + note = -1 + if !btnA.Get() { + note = 0 // C4 + } + if !btnB.Get() { + note = 1 // E4 + } + if !btnRot.Get() { + note = 2 // G4 + } + + if note != oldNote { + if oldNote != -1 { + midi.Midi.NoteOff(0, midichannel, notes[oldNote], 50) + } + if note != -1 { + midi.Midi.NoteOn(0, midichannel, notes[note], 50) + } + oldNote = note + } + + time.Sleep(100 * time.Millisecond) + } +} diff --git a/tutorial/ble/step1/main.go b/tutorial/ble/step1/main.go new file mode 100644 index 0000000..30db9fa --- /dev/null +++ b/tutorial/ble/step1/main.go @@ -0,0 +1,145 @@ +package main + +// BLE counter example using the Nordic UART Service (NUS). +// Compatible apps: nRF Toolbox, Serial Bluetooth Terminal (Android/iOS). +// - Subscribe to TX notifications to receive the counter value every second. +// - Send "reset" to the RX characteristic to reset the counter to zero. + +import ( + "image/color" + "machine" + "strconv" + "time" + + "github.com/tinygo-org/bluetooth" + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freesans" +) + +// Nordic UART Service UUIDs (6E400001-B5A3-F393-E0A9-E50E24DCCA9E) +var ( + adapter = bluetooth.DefaultAdapter + + serviceUUID = bluetooth.NewUUID([16]byte{ + 0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93, + 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E, + }) + rxUUID = bluetooth.NewUUID([16]byte{ + 0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93, + 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E, + }) + txUUID = bluetooth.NewUUID([16]byte{ + 0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93, + 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E, + }) +) + +var ( + display st7789.Device + connected bool + counter int +) + +var ( + black = color.RGBA{0, 0, 0, 255} + cyan = color.RGBA{0, 200, 255, 255} + yellow = color.RGBA{255, 220, 0, 255} + gray = color.RGBA{150, 150, 150, 255} +) + +func main() { + initDisplay() + drawStatus("Advertising...") + drawCounter(0) + + must("enable BLE", adapter.Enable()) + + var txChar bluetooth.Characteristic + + must("add service", adapter.AddService(&bluetooth.Service{ + UUID: serviceUUID, + Characteristics: []bluetooth.CharacteristicConfig{ + { + UUID: rxUUID, + Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + cmd := string(value) + if cmd == "reset" || cmd == "reset\n" { + counter = 0 + } + }, + }, + { + Handle: &txChar, + UUID: txUUID, + Flags: bluetooth.CharacteristicNotifyPermission | bluetooth.CharacteristicReadPermission, + }, + }, + })) + + adv := adapter.DefaultAdvertisement() + must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: "NiceBadge", + ServiceUUIDs: []bluetooth.UUID{serviceUUID}, + })) + must("start adv", adv.Start()) + + for { + counter++ + drawCounter(counter) + + // Write sends a BLE notification to subscribed centrals. + // If no device is subscribed, Write returns an error. + _, err := txChar.Write([]byte(strconv.Itoa(counter) + "\n")) + + wasConnected := connected + connected = err == nil + if wasConnected != connected { + if connected { + drawStatus("Connected ") + } else { + drawStatus("Advertising...") + must("restart adv", adv.Start()) + } + } + + time.Sleep(time.Second) + } +} + +func initDisplay() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + display = st7789.New(machine.SPI0, + machine.P1_15, machine.P1_13, machine.P0_10, machine.P0_09) + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + display.FillScreen(black) +} + +func drawStatus(status string) { + display.FillRectangle(0, 0, 240, 32, black) + tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 22, "BLE: "+status, cyan) +} + +func drawCounter(n int) { + display.FillRectangle(0, 38, 240, 97, black) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 68, "Counter", gray) + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, strconv.Itoa(n), yellow) +} + +func must(action string, err error) { + if err != nil { + panic("failed to " + action + ": " + err.Error()) + } +} diff --git a/tutorial/ble/step2/main.go b/tutorial/ble/step2/main.go new file mode 100644 index 0000000..688dce5 --- /dev/null +++ b/tutorial/ble/step2/main.go @@ -0,0 +1,143 @@ +package main + +// BLE LED color control example. +// A mobile app writes 3 bytes (R, G, B) to the color characteristic +// and both WS2812 LEDs light up with that color. +// +// Service UUID: BADA5501-B5A3-F393-E0A9-E50E24DCCA9E +// Color char: BADA5502-B5A3-F393-E0A9-E50E24DCCA9E +// +// Compatible apps: nRF Connect, LightBlue (iOS/Android). +// To set color: write 3 hex bytes to the color characteristic, e.g. FF0080 = red:255 green:0 blue:128. + +import ( + "image/color" + "machine" + "strconv" + + "github.com/tinygo-org/bluetooth" + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/drivers/ws2812" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freesans" +) + +var ( + adapter = bluetooth.DefaultAdapter + + ledServiceUUID = bluetooth.NewUUID([16]byte{ + 0xBA, 0xDA, 0x55, 0x01, 0xB5, 0xA3, 0xF3, 0x93, + 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E, + }) + colorCharUUID = bluetooth.NewUUID([16]byte{ + 0xBA, 0xDA, 0x55, 0x02, 0xB5, 0xA3, 0xF3, 0x93, + 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E, + }) +) + +var ( + display st7789.Device + leds ws2812.Device + connected bool + ledColor color.RGBA +) + +var ( + black = color.RGBA{0, 0, 0, 255} + cyan = color.RGBA{0, 200, 255, 255} + white = color.RGBA{255, 255, 255, 255} + gray = color.RGBA{150, 150, 150, 255} +) + +func main() { + initDisplay() + initLEDs() + drawStatus("Advertising...") + drawColor(color.RGBA{0, 0, 0, 255}) + + must("enable BLE", adapter.Enable()) + + must("add service", adapter.AddService(&bluetooth.Service{ + UUID: ledServiceUUID, + Characteristics: []bluetooth.CharacteristicConfig{ + { + UUID: colorCharUUID, + Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + if len(value) < 3 { + return + } + // value[0]=R, value[1]=G, value[2]=B + ledColor = color.RGBA{value[0], value[1], value[2], 255} + setLEDs(ledColor) + drawColor(ledColor) + + if !connected { + connected = true + drawStatus("Connected ") + } + }, + }, + }, + })) + + adv := adapter.DefaultAdvertisement() + must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: "NiceBadge", + ServiceUUIDs: []bluetooth.UUID{ledServiceUUID}, + })) + must("start adv", adv.Start()) + + // wait forever; all logic is driven by the BLE WriteEvent callback + select {} +} + +func initDisplay() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + display = st7789.New(machine.SPI0, + machine.P1_15, machine.P1_13, machine.P0_10, machine.P0_09) + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + display.FillScreen(black) +} + +func initLEDs() { + neo := machine.P1_11 + neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) + leds = ws2812.New(neo) +} + +func setLEDs(c color.RGBA) { + leds.WriteColors([]color.RGBA{c, c}) +} + +func drawStatus(status string) { + display.FillRectangle(0, 0, 240, 32, black) + tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 22, "BLE: "+status, cyan) +} + +func drawColor(c color.RGBA) { + // show a filled rectangle with the current color and its RGB values below + display.FillRectangle(0, 38, 240, 60, color.RGBA{c.R, c.G, c.B, 255}) + display.FillRectangle(0, 100, 240, 35, black) + rgb := "R:" + strconv.Itoa(int(c.R)) + + " G:" + strconv.Itoa(int(c.G)) + + " B:" + strconv.Itoa(int(c.B)) + tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 122, rgb, white) +} + +func must(action string, err error) { + if err != nil { + panic("failed to " + action + ": " + err.Error()) + } +} diff --git a/tutorial/ble/step3/main.go b/tutorial/ble/step3/main.go new file mode 100644 index 0000000..efbffd0 --- /dev/null +++ b/tutorial/ble/step3/main.go @@ -0,0 +1,150 @@ +package main + +// BLE scanner example. +// Scans for nearby BLE devices and shows their names, addresses and signal +// strength (RSSI) on the display. Press button A to clear the list. + +import ( + "image/color" + "machine" + "strconv" + "time" + + "github.com/tinygo-org/bluetooth" + "tinygo.org/x/drivers/st7789" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freesans" +) + +const maxDevices = 5 + +type bleDevice struct { + name string + addr string + rssi int16 +} + +var ( + adapter = bluetooth.DefaultAdapter + display st7789.Device + devices [maxDevices]bleDevice + count int +) + +var ( + black = color.RGBA{0, 0, 0, 255} + cyan = color.RGBA{0, 200, 255, 255} + white = color.RGBA{255, 255, 255, 255} + yellow = color.RGBA{255, 220, 0, 255} + green = color.RGBA{0, 220, 100, 255} + gray = color.RGBA{150, 150, 150, 255} +) + +func main() { + initDisplay() + + btnA := machine.P1_06 + btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + + must("enable BLE", adapter.Enable()) + + drawHeader() + + // scan runs in a goroutine; found devices are stored and displayed in the main loop + go func() { + adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) { + name := result.LocalName() + if name == "" { + name = result.Address.String() + } + + // update existing entry if already seen, otherwise add new + found := false + for i := 0; i < count; i++ { + if devices[i].addr == result.Address.String() { + devices[i].rssi = result.RSSI + found = true + break + } + } + if !found && count < maxDevices { + devices[count] = bleDevice{ + name: name, + addr: result.Address.String(), + rssi: result.RSSI, + } + count++ + } + }) + }() + + for { + // button A clears the list + if !btnA.Get() { + count = 0 + display.FillRectangle(0, 32, 240, 103, black) + time.Sleep(200 * time.Millisecond) + } + + drawDevices() + time.Sleep(500 * time.Millisecond) + } +} + +func initDisplay() { + machine.SPI0.Configure(machine.SPIConfig{ + SCK: machine.P1_01, + SDO: machine.P1_02, + Frequency: 8000000, + Mode: 0, + }) + display = st7789.New(machine.SPI0, + machine.P1_15, machine.P1_13, machine.P0_10, machine.P0_09) + display.Configure(st7789.Config{ + Rotation: st7789.ROTATION_90, + Width: 135, + Height: 240, + RowOffset: 40, + ColumnOffset: 53, + }) + display.FillScreen(black) +} + +func drawHeader() { + tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 24, "BLE Scanner", cyan) +} + +func drawDevices() { + // clear device list area + display.FillRectangle(0, 32, 240, 103, black) + + if count == 0 { + tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 55, "No devices found...", gray) + return + } + + for i := 0; i < count; i++ { + y := int16(52 + i*20) + name := devices[i].name + if len(name) > 16 { + name = name[:16] + } + rssi := " " + strconv.Itoa(int(devices[i].rssi)) + "dB" + + // color by signal strength + c := white + if devices[i].rssi > -60 { + c = green + } else if devices[i].rssi > -80 { + c = yellow + } + + tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 5, y, name+rssi, c) + } +} + +func must(action string, err error) { + if err != nil { + panic("failed to " + action + ": " + err.Error()) + } +} diff --git a/tutorial/go.mod b/tutorial/go.mod new file mode 100644 index 0000000..f030f82 --- /dev/null +++ b/tutorial/go.mod @@ -0,0 +1,3 @@ +module code.madriguera.me/GoEducation/nicebadge/tutorial + +go 1.22.1 diff --git a/tutorial/go.sum b/tutorial/go.sum new file mode 100644 index 0000000..e69de29