Hijacking infrared to make a dumb device smart

Saturday, November 26, 2022

I own a very old ‘flat’ screen television from 2009. One of the reason this TV is still kicking around is because I have a strange sentimental affection for it. Another is that I have written automation for it that works really well and it would be a pain to migrate away from it.

It does have a problem however, the TV tends to hit resonance frequency very often with its built-in speakers, causing the body of the unit to vibrate something awful and produce some ear wrenching sounds as a result.

So, to extend the longevity of this relic, I decided to invest in a cheapo soundbar, the Majority Snowdon II . This has worked to fix the resonance issue but has somewhat moved my problems laterally as it came with its own caveats. Namely, its a big dumb dumb.

That’s right, it’s not a smart device, its about as dumb as they come in 2022. It has an infrared remote, some physical buttons and that’s it! However, I had a plan when I bought this device. I thought I could probably make it smarter. So after losing the remote down the side of the couch for the 87th time I decided to crack it open and see what could be done.

Table of Contents

Start

My original intentions when cracking this open was to start looking at datasheets for on-board chips to try and find a foothold somewhere on the board. However, looking at the mainboard, I was very quickly struck with an idea.

There are two very cleanly labelled connection jacks that go to the daughter board. And the daughter board just happens to host all of the interfacing options on the device: the physical buttons, an indicator LED and the infrared receiver.

So lets just hijack these pre-existing interfaces for our own purposes!

Plan of Attack

Now that we know the project has some feasbility, lets lay the ground rules for what success will look like. We want to be able to

  • Get the current state of Snowdon using the RGB lines from the status LED
  • Mimic the signal coming in on the Infrared line so that we can send our own infrared commands to Snowdon

We also want to have control over the new super powers we will bestow upon the Snowdon. At this point, I had already earmarked the Raspberry Pi Pico W as the microcontroller of choice for this project for a few reasons:

  1. It has a WiFi chip, which means we can turn the snowdon into a true IoT device
  2. It has Programmable IO , which means we can write our own driver for the Infrared signalling.
  3. It accepts 5v power in, which means we can power it directly from the mainboard
  4. I had a bunch of them in my desk drawer 😀

Decoding the Indicator LED

The daughter board has a 4 pin RGB indicator LED that can display a number of colors. The LED is common anode which means it is active low. Each of the 3 color pins are broken out on a connection jack on the mainboard. The user manual actually tells us the possible colors and their meaning too:

State LED Indicator Light
Power Off Red
AUX Mode White
Line In Mode Green
Optical Mode Yellow
Bluetooth Ready Flashing Blue
Bluetooth Connected Blue

This is enough information for us to decode which color is currently being displayed on the LED and infer a state from it.

We can achieve this easily by wiring 3 consecutive GPIO pins to the mainboard.

Wiring

S G n B R o L E R w U E E d E N D o _ _ _ n G L L L + N E E E 5 D D D D V R G G G G V a N P P P S s D I I I Y p O O O S P 1 1 1 i 9 8 7

Code

Since we are only concerned with 3 pins (17,18 & 19), we can create a 32 bit bit-mask that will select only these pins:

#define RGB_BASE_PIN 17
const uint32_t RGB_MASK = 1 << RGB_BASE_PIN |     // Pin 17
                          1 << RGB_BASE_PIN + 1 | // Pin 18
                          1 << RGB_BASE_PIN + 2;  // Pin 19

Then in our main loop we can initialise the pins as input with our mask:

gpio_init_mask(RGB_MASK);

And then get the current value of each of the 3 pins by ANDing the value of the GPIO register with our mask. We then shift this to the right to clear out the left over zeros from our AND operation and end up with a 3 bit value representing our 3 color lines:

// Extract desired bits from GPIO with RGB_MASK and shift right
// This gives us a 3 bit value in the form 0b<b><g><r>
uint32_t gpio = (gpio_get_all() & RGB_MASK) >> RGB_BASE_PIN;

Finally, we can pass this value into a switch statement and based on the value of the 3 color bits, we can print a different message:

switch(gpio) {
    case 0b110: // red
        printf("off\n");
        break;
    case 0b100: // yellow
        printf("optical\n");
        break;
    case 0b000: // white
        printf("aux\n");
        break;
    case 0b101: // green
        printf("line-in\n");
        break;
    case 0b011: // blue
        printf("bluetooth\n");
        break;
    case 0b111: // off
        printf("none\n");
        break;
    default:
        printf("unknown\n");
    }
Full code
#include <stdio.h>
#include "pico/stdlib.h"

#define RGB_BASE_PIN 17
const uint32_t RGB_MASK = 1 << RGB_BASE_PIN |        // Pin 17
                          1 << RGB_BASE_PIN + 1 |    // Pin 18
                          1 << RGB_BASE_PIN + 2;     // Pin 19

int main() {
    stdio_init_all();
    // Enable pins 17, 18 & 19 as input 
    gpio_init_mask(RGB_MASK);
    uint32_t gpio;
    while (true) {
        // Extract desired bits from GPIO with RGB_MASK and shift right
        // This gives us a 3 bit value in the form 0b<b><g><r>
        gpio = (gpio_get_all() & RGB_MASK) >> RGB_BASE_PIN;
        // Perform comparisons on the 3 bits to determine the state of the RGB LED
        switch(gpio) {
            case 0b110: // red
                printf("off\n");
                break;
            case 0b100: // yellow
                printf("optical\n");
                break;
            case 0b000: // white
                printf("aux\n");
                break;
            case 0b101: // green
                printf("line-in\n");
                break;
            case 0b011: // blue
                printf("bluetooth\n");
                break;
            case 0b111: // off
                printf("none\n");
                break;
            default:
                printf("unknown\n");
        }
        sleep_ms(500);
    }
    return 0;
}

Demo

This simulation demonstrates our ability to decode the color of an RGB LED. The switches on the breadboard can be toggled to ‘hardcode’ an RGB value which our program will then decode on a 500ms timer.

Mimicing an Infrared Remote

The infrared (IR) protocol used by the Snowdon and indeed in the majority of consumer products is called NEC. When you press a button on your remote, a single NEC message will be sent, carrying 32 bits (uint32_t) of information:

  • 8 bit device address
  • 8 bit device address (logical inverse)
  • 8 bit command
  • 8 bit command (logical inverse)

The full message looks like this:

And consists of the following:

  • 9ms (562.5us x 16) leading LOW pulse
  • 4.5ms (562.5us x 8) HIGH pulse
  • 32 bits of information
  • 562.5us trailing LOW pulse

Each bit in the message starts with a LOW pulse for 562.5us, followed by:
  • If the bit to be encoded is LOW, a HIGH pulse for 562.5us
  • If the bit to be encoded is HIGH, a HIGH pulse for 1.6ms (562.5us x 3)

It is worth noting there are a few nuances with the NEC protocol when transmitting normally via an LED:

  • When sent via an LED the message is inverted to what we see in the diagram
  • When sent via an LED the message is modulated with a 38khz carrier wave

We can safely ignore both of these facts because we will be circumventing the usual front door of an IR LED and directly connecting our Pico to the receiving IR line on the Snowdon mainboard.

Now that we understand a little about the NEC protocol, we can wire up the Pico to the Snowdon’s IR line

Wiring

S n o G + w I N 5 d R D V o n 2 2 0 R G G V a P N S s I D Y p O S P 1 i 6

The 220Ω resistor is required to give priority to the IR transceiver on the daughterboard. Otherwise legitimate IR codes sent via the remote control may get dropped

Code

To achieve the timing requirements of the protocol, we are going to be writing a Programmable IO (PIO) assembly program that will take a 32-bit unsigned integer (uint32_t) as input, translate it into a a NEC formatted message and broadcast it on GPIO 16.

PIO is a bit of a hard nut to crack so here are some suggested materials if its your first PIO rodeo:


The first thing we are going to configure for our driver is side set. Side set allows us to drive up to 5 consecutive pins as a side effect of a PIO ASM instruction. For our purposes we are only interested in driving a single GPIO pin with our IR data, so we will declare this to the compiler with a label:
.side_set 1

Side setting steals bits from the delay function in PIO. Stealing 1 bit for side setting, as we are doing, reduces this maximum delay value from 31 ticks to 15 ticks


In our init function we also need to perform some setup to assosiate the variable pin (GPIO 16) with the side set function:
sm_config_set_sideset_pins(&c, pin);

We also need to perform some setup functions to enable our pin as output and give it an initial value:
pio_gpio_init(pio, pin);                                // Set pin function to GPIO
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);  // Set the pin direction to output 
pio_sm_set_pins_with_mask(pio, sm, 1u << pin, 1);       // Set the initial value of the pin to 1 (HIGH)
gpio_pull_up(pin);                                      // Set the default value of the pin to 1 (HIGH)

Lastly, we need to configure the clock. Looking at the timing diagram, our first instinct may be to set the clock to ~560us. so that each PIO instruction takes ~560us to execute. However, as will become evident later on, we actually need the flexibility to perform 2 instructions per ~560us window. So that is what we will set the clock to:
// 2 ticks per 560us window 
float div = clock_get_hz(clk_sys) / (2 * (1 / 562.5e-6f));
sm_config_set_clkdiv(&c, div);

The body of the PIO program looks like this:

.wrap_target
    pull side 1
pulse_init:
    nop side 0 [15] 
    nop side 0 [15]         ; 9ms on 
    nop side 1 [15]         ; 4.5ms delay
next:
    out y 1 side 0          ; Read next bit from OSR into y, side set LOW for 1 tick (280us)
    jmp !y short side 0     ; If y == 0, goto short,  side set LOW for 1 tick (280us)
long:
    jmp bit_loop side 1 [4] ; Side set HIGH for 5 ticks (1400us)
short:
    nop side 1              ; Side set HIGH for 1 tick (280us)
bit_loop:
    jmp !osre next side 1   ; goto next if osr is not empty, side set HIGH for 1 tick (280us)
end_pulse:
    nop side 0 [1]          ; Side set LOW for 2 ticks (560us)
.wrap

Let’s disect this to understand how we are achieving NEC transmission.

pull side 1

The pull instruction will pull 32 bits into the program as input. This call will block until we give the program our uint32_t to encode. Additionally we:

  • Use side set to drive GPIO 16 HIGH, this will remain HIGH whilst the pull instruction is blocked

pulse_init:
    nop side 0 [15] 
    nop side 0 [15]         ; 9ms on 
    nop side 1 [15]         ; 4.5ms delay

After the user provides input, execution continues and we enter the pulse_init label, where we:

  • Execute the nop instruction which does nothing for a single tick.
  • Delay each nop for 15 ticks, such that each instruction takes 16 ticks total.
  • Use side set to drive GPIO 16 LOW for 32 ticks (9ms) and then HIGH for 16 ticks (4.5ms).
next:
    out y 1 side 0          ; Read next bit from OSR into y, side set LOW for 1 tick (280us)
    jmp !y short side 0     ; If y == 0, goto short,  side set LOW for 1 tick (280us)

We fall through to the next label, where we:

  • Pop one bit of our input into the y register.
  • Do a conditional jump, if the bit’s value is 0 we jump to the short label.
  • Side set LOW for both instructions, achieving our initial LOW pulse for the first bit.

long:
    jmp bit_loop side 1 [4] ; Side set HIGH for 5 ticks (1400us)

If we did not conditionally jump, then we fall through to the long label, where we:

  • Unconditionally jump to the bitloop label.
  • Side set HIGH with a 4 tick delay, totalling 5 ticks

short:
    nop side 1              ; Side set HIGH for 1 tick (280us)

Else, we conditionally jumped to the short label, where we:

  • Do nothing (nop). Due to the positioning of our labels we can simply fall through to the bit_loop label
  • Side set HIGH on GPIO 16 for a single tick

bit_loop:
    jmp !osre next side 1   ; goto next if osr is not empty, side set HIGH for 1 tick (280us)

Regardless of our branching path, we end up in the bit_loop label. Where we:

  • Conditionally jump back up to the next label as long as we still have input bits left to transcode
  • Side set HIGH on GPIO 16 for a single tick. This means we have driven the GPIO HIGH for 6 ticks if we got here via long, or 2 ticks via short!

We then start again from next until we have processed all 32 bits:

end_pulse:
    nop side 0 [1]          ; Side set LOW for 2 ticks (560us)

After all bits have been exhausted, we finally enter the end_pulse label, where we:

  • Do nothing for a single tick
  • Side set LOW on GPIO 16 for 2 ticks, achieving our trailing pulse
  • Wrap back around to the first pull instruction to wait for next input

Full code
; Implements an inverted NEC infrared protocol WITHOUT carrier signal
; For use in wired connection to IR line.  Each instruction is 280us
.program nec
.side_set 1
.wrap_target
    pull side 1
pulse_init:
    nop side 0 [15] 
    nop side 0 [15]         ; 9ms on 
    nop side 1 [15]         ; 4.5ms delay
next:
    out y 1 side 0          ; Read next bit from OSR into y, side set LOW for 1 tick (280us)
    jmp !y short side 0     ; If y == 0, goto short,  side set LOW for 1 tick (280us)
long:
    jmp bit_loop side 1 [4] ; Side set HIGH for 5 ticks (1400us)
short:
    nop side 1              ; Side set HIGH for 1 tick (280us)
bit_loop:
    jmp !osre next side 1   ; goto next if osr is not empty, side set HIGH for 1 tick (280us)
end_pulse:
    nop side 0 [1]          ; Side set LOW for 2 ticks (560us)
.wrap

% c-sdk {
#include "hardware/clocks.h"
static inline void nec_transmit_program_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = nec_program_get_default_config(offset);
    sm_config_set_sideset_pins(&c, pin);

    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
    pio_sm_set_pins_with_mask(pio, sm, 1u << pin, 1);
    gpio_pull_up(pin);
    
    sm_config_set_out_shift(&c, true, false, 32);
    
    // 2 ticks per 560us window 
    float div = clock_get_hz(clk_sys) / (2 * (1 / 562.5e-6f));
    sm_config_set_clkdiv(&c, div);

    // Init the pio state machine with PC at offset
    pio_sm_init(pio, sm, offset, &c);
    // Start sm
    pio_sm_set_enabled(pio, sm, true);
}
%}

Demo

This simulation demonstrates our driver’s ability to mimic an infrared remote. When the switch is toggled left, it will accept input directly from the IR remote via the IR receiver.

However, when the switch is toggled to the right, we see that our PIO program is sending a random remote code out on GPIO 16 every 500ms:

Code
... 
#define TX_PIN 16
...
uint32_t remote_codes[] = {
    0x5da2ff00,  //POWER                                                            
    0xdd22ff00,  //TEST                                                             
    0xfd02ff00,  //PLUS                                                             
    0x3dc2ff00,  //BACK                                                             
    0x1de2ff00,  //MENU
    0x6f90ff00,  //NEXT
    0x57a8ff00,  //PLAY
    0x1fe0ff00,  //PREV
    0x9768ff00,  //0
    0x6798ff00,  //MINUS
    0x4fb0ff00,  //C
    0x857aff00,  //3
    0xe718ff00,  //2
    0xcf30ff00,  //1
    0xef10ff00,  //4
    0xc738ff00,  //5
    0xa55aff00,  //6
    0xad52ff00,  //9
    0xb54aff00,  //8
    0xbd42ff00,  //7
};
...
while (true) {
    pio_sm_put_blocking(PIO_INSTANCE, tx_sm, 
                        remote_codes[rand() % ARRAY_SIZE(remote_codes)]);
...
    sleep_ms(500);
}

Building a dunderhead HTTP server from scratch

At the time that I started this project (September 2022) there were no examples online, that I could find, of a HTTP server that utilised the C SDK for the Pico W. I did however know from some prior adventures into ESP32, that you can remain very stupid as a microcontroller and still talk HTTP, so lets roll our own HTTP server!

The way I achieved this was by building on the tcp server example over on the official pico-examples github repository.

What even is HTTP anyway?

Hypertext Transfer Protocol (HTTP) at is core is just a TCP socket with fancy strings.
Theres a great crash course by @fasterthanlime over on his blog that I would recommend reading.

So how do we send fancy strings?

Let’s send a standard HTTP PUT request via the cli tool curl and see what it looks like, request:

curl -X PUT http://api.int/api/v1.0/pc\?code\=status

Response:

{"status": "on"}

Plucking the request out of wireshark, we can see that the actual TCP payload contains the following information:

PUT /api/v1.0/pc?code=status HTTP/1.1\r\n
Host: api.int\r\n
User-Agent: curl/7.68.0\r\n
Accept: */*\r\n
\r\n

Let’s disect this. The first line specifies a few things:

Parameter Value Description
Method GET The method of the call, common methods are GET, POST, PUT, HEAD
Target /api/v1.0/pc?code=status Specifies the sub-path target of the call, this allows routing in a HTTP server that hosts many different endpoints. key/value pairs can also be specified in this line
Version HTTP/1.1 This is the HTTP version of the call, for our purposes it will always be HTTP/1.1
  • Each line end is denoted with a carriage return \r and line feed \n.
  • Each line after the first contains a header variable in the form of a key/value pair.
  • The final line only contains \r\n to denote the end of the data

The response data looks like this:

HTTP/1.1 200 OK\r\n
Content-Length: 17\r\n
Content-Type: application/json\r\n
Date: Wed, 30 Nov 2022 10:40:24 GMT\r\n
Server: waitress\r\n
\r\n
{"status": "on"}

It follows a very similar format, the first line again specifies a few things:
Parameter Value Description
Version HTTP/1.1 This is the HTTP version of the call, for our purposes it will always be HTTP/1.1
Status Code 200 A numeric representation of the response status, must be a valid HTTP status code as documented here
Status Text OK A textual representation of the response status, must be paired up with the status code

Again each preceding line is a key/value header variable. However it is worth noting that the HTTP server chose to send a JSON response.

To pull this off, the response had to contain the header Content-Length, to inform the client of how much data it was going to send. After which it appends the JSON string after the final \r\n line of the HTTP body.

This concept of setting the Content-Length header applies to the client too if we wish to send JSON in our request.

This protocol is actually simple enough that we can pick apart a HTTP request and construct responses just by doing some simple string manipulation. We don’t even need to understand anything about HTTP or JSON to speak the lingo!

Code

The first (and hardest) thing we need to do in our little HTTP server is process HTTP request messages.

We can achieve this by breaking the string up into it’s individual tokens. We are going to use the string function strpbrk to achieve this.

The man page has this to say on the strpbrk function:

SYNOPSIS
       #include <string.h>

       char *strpbrk(const char *s, const char *accept);

DESCRIPTION
       The  strpbrk() function locates the first occurrence in the string s of
       any of the bytes in the string accept.

RETURN VALUE
       The strpbrk() function returns a pointer to the byte in s that  matches
       one of the bytes in accept, or NULL if no such byte is found.

So this function takes a string and a set of deliminators, and outputs a pointer to the first deliminator it encounteres in the string.

If we look again at the top line of the HTTP request, there are indeed a fixed set of single character deliminators that denote certain parts of the line:

PUT /api/v1.0/pc?code=status HTTP/1.1\r\n

So we can extract a given token by performing a set of steps.

Define our message in a string (char *) and create a pointer to the first character:

char http_message[] = {
    "PUT /api/v1.0/pc?code=status HTTP/1.1\r\n"
    "Host: api.int\r\n"
    "User-Agent: curl/7.68.0\r\n"
    "Accept: */*\r\n"
    "\r\n"
};
char *p1 = http_message;

Call the strpbrk function to obtain a pointer to the first instance of one of our deliminators:

char *p2 = strpbrk(p1, " ?=\r");

Now lets null terminate p2. By placing \0 at the position earmarked by p2, we transform the deliminator into the last character of the string:

*p2 = '\0';

If we now print p1, it will only print up to the first \0, essentially turning it into our first token:

printf("%s\n", p1);
> PUT

We can further extend this technique to iterate over each token in the first line of the HTTP request, and by keeping track of the deliminators we can tell exactly which token we just extracted.


Full code
#include <string.h>
#include <stdio.h>


int main() {
  char http_message[] = {
    "PUT /api/v1.0/pc?other=monkey&code=status HTTP/1.1\r\n"
    "Host: api.int\r\n"
    "User-Agent: curl/7.68.0\r\n"
    "Accept: */*\r\n"
    "\r\n"
  };

  // Process HTTP message body, example: "PUT /api/v1.0/pc?code=status HTTP/1.1"
  // char *message_body = http_message;
  char *message_body = http_message;
  char *delim = " ?=&\r";
  char next_delim;
  char current_delim = '\0';
  char *token;

  while(1) {
    if (*message_body == '\0') { break; }
    token = message_body;
    message_body = strpbrk(message_body, delim);
    if (message_body == NULL) { break; }
    next_delim = *message_body;
    *message_body = '\0';

    switch(current_delim) {
      case '\0':
        printf("Method\t\t%s\n", token);
        break;
      case ' ':
        if (next_delim != '\r') {
          printf("Target\t\t%s\n", token);
        } else {
          printf("Version\t\t%s\n", token);
        }
        break;
      case '?':
      case '&':
        printf("Key\t\t%s\n", token);
        break;
      case '=':
        printf("Value\t\t%s\n", token);
        break;
    }
    current_delim = next_delim;
    message_body++;
    // Carriage return indicates we have reached the end of the first line of message body
    if (current_delim == '\r') { break; }
  }

  return 0;
}

Within the final program we actually take this one step further and use the same technique to step through JSON as a flat file if it is provided as part of the HTTP request.

Demo

Optional: Recycling unused pins for SWD debugging

If we take a look at the mainboard again, we can see there is a 4 pin “USB” header exposed on the board with unused pins:

I tested both NONE pins on the oscilloscope and determined that there was no activity on either pin during normal operation. As it turns out the panel on the back actually has a cut out for the USB header, which is only covered by a thin layer of mylar tape that can be cut away:


Due to this I decided to wire up the SWD debugging pins from the Pico to the unused pins so that I could perform debugging without having to re-open the unit:

S n o w N N d O O o N N n G E E + N _ _ 5 D 1 2 V R G S S V a N W W S s D D C Y p I L S O K P i

I would highly recommend NOT doing this, as I managed to fry the SWD pins on one of my debuggers using these pins and I am not 100% as to the reason

Tying it all together

Throughout this adventure we have developed some interesting capabilities on our Pico, we can now:

  • Get the current state of the Snowdon via the RGB LED
  • Send our own NEC encoded commands to the Snowdon
  • Decode arbitrary HTTP requests

To bring this all together we only really need a little bit of glue code to translate a HTTP request into a call to our PIO driver or LED decoder.


This is achieve in the program by extracting a value from one of the user provided parameters passed in the HTTP request (code) and passing this into a helper function. The function translates the user provided string into a 32 bit NEC code for our PIO driver, or a HTTP_CODE_LOOKUP_STATUS command for our LED decoder:

static uint32_t http_code_lookup(char *code) {
    if (!strcmp(code, "status"))        { return HTTP_CODE_LOOKUP_STATUS; }
    if (!strcmp(code, "power"))         {return 0x807F807F;}
    if (!strcmp(code, "input"))         {return 0x807F40BF;}
    if (!strcmp(code, "mute"))          { return 0x807FCC33; }
    if (!strcmp(code, "volume_up"))     { return 0x807FC03F; }
    if (!strcmp(code, "volume_down"))   { return 0x807F10EF; }
    if (!strcmp(code, "previous"))      { return 0x807FA05F; }
    if (!strcmp(code, "next"))          { return 0x807F609F; }
    if (!strcmp(code, "play_pause"))    { return 0x807FE01F; }
    if (!strcmp(code, "treble_up"))     { return 0x807FA45B; }
    if (!strcmp(code, "treble_down"))   { return 0x807FE41B; }
    if (!strcmp(code, "bass_up"))       { return 0x807F20DF; }
    if (!strcmp(code, "bass_down"))     { return 0x807F649B; }
    if (!strcmp(code, "pair"))          { return 0x807F906F; }
    if (!strcmp(code, "flat"))          { return 0x807F48B7; }
    if (!strcmp(code, "music"))         { return 0x807F946B; }
    if (!strcmp(code, "dialog"))        { return 0x807F54AB; }
    if (!strcmp(code, "movie"))         { return 0x807F14EB; }
    return HTTP_CODE_LOOKUP_UNKNOWN_VALUE;
}

We then just make the respective call in our program and return a nice hard coded JSON string in our HTTP response:

if (state->message_body->code == HTTP_CODE_LOOKUP_STATUS) {
    uint32_t gpio;
    do{
        gpio = (gpio_get_all() & RGB_MASK) >> RGB_BASE_PIN;
        switch(gpio) {
            case 0b110: // red
                http_generate_response(arg, "{\"onoff\": \"off\", \"input\": \"off\"}\n", "200 OK");
                break;
            case 0b100: // yellow
                http_generate_response(arg, "{\"onoff\": \"on\", \"input\": \"optical\"}\n", "200 OK");
                break;
            case 0b000: // white
                http_generate_response(arg, "{\"onoff\": \"on\", \"input\": \"aux\"}\n", "200 OK");
                break;
            case 0b101: // green
                http_generate_response(arg, "{\"onoff\": \"on\", \"input\": \"line-in\"}\n", "200 OK");
                break;
            case 0b011: // blue
                http_generate_response(arg, "{\"onoff\": \"on\", \"input\": \"bluetooth\"}\n", "200 OK");
                break;
            case 0b111: // off (likely in a transitioning state)
                busy_wait_ms(50);
                continue;
        }
    } while (gpio == 0b111);
    return;
}
if (state->message_body->code > HTTP_CODE_LOOKUP_NO_VALUE) {
    pio_sm_put_blocking(PIO_INSTANCE, 0, state->message_body->code);
    http_generate_response(arg, "{\"status\": \"ok\"}\n", "200 OK");
}

This is essentially all the building blocks required to roll our own RESTful API. With that out of the way, all that’s left to do now is wire it up and compile!

Wiring

SWDIO and SWCLK are not required for normal operations, they expose the SWD debug pins on the unused USB header for easy access

S G n B R o L E R w I U E E N N d R E N D O O o _ _ _ _ N N n L L L L E E + G E E E E _ _ 5 N D D D D 1 2 V D 2 2 0 R G G G G S S V G a P P P P W W S N s I I I I D C Y D p O O O O I L S O K P 1 1 1 1 i 6 9 8 7

Compiling Guide

Please refer to the offical Getting Started guide for full details on getting a build environment setup

Due to the final software containing hardcoded WiFi credentials, it’s impossible to share a pre-built uf2. So we must install the Pico SDK and built it from scratch.


Install dependencies for SDK:

sudo apt update
sudo apt install git cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential

Clone SDK;

cd ~/
git clone https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init

Export PICO_SDK_PATH variable:

export PICO_SDK_PATH=$HOME/pico-sdk

Clone Snowdon-II-WiFi:

git clone https://github.com/kennedn/snowdon-ii-wifi.git

Create a build directory and configure cmake with WiFi credentials:

cd snowdon-ii-wifi
mkdir build
cd build
# Replace <SSID> and <PASSWORD> with own values
cmake -DPICO_BOARD=pico_w -DWIFI_SSID="<SSID>" -DWIFI_PASSWORD="<PASSWORD>" ..

Compile the program:

cd src
make

If all goes well, a file named snowdon.uf2 should now exist under ~/snowdon-ii-wifi/build/src/.


The Pico can now be plugged in via USB whilst holding down the BOOTSEL button, and the uf2 file dropped in the volume mount.

API

The RESTful API is exposed on port 8080:

http://<ip_address>:8080


The endpoint expects a single code parameter, which can be sent via either url encoding or in the JSON body of the request, e.g:

curl -X PUT http://192.168.1.238:8080?code=status
# or
curl -X PUT http://192.168.1.238:8080 -H 'Content-Type: application/json' -d '{"code": "power"}'

And this is the full list of available code values:

Value Description JSON response
power Infrared Code {"status": "ok"}
input Infrared Code {"status": "ok"}
mute Infrared Code {"status": "ok"}
volume_up Infrared Code {"status": "ok"}
volume_down Infrared Code {"status": "ok"}
previous Infrared Code {"status": "ok"}
next Infrared Code {"status": "ok"}
play_pause Infrared Code {"status": "ok"}
treble_up Infrared Code {"status": "ok"}
treble_down Infrared Code {"status": "ok"}
bass_up Infrared Code {"status": "ok"}
bass_down Infrared Code {"status": "ok"}
pair Infrared Code {"status": "ok"}
flat Infrared Code {"status": "ok"}
music Infrared Code {"status": "ok"}
dialog Infrared Code {"status": "ok"}
movie Infrared Code {"status": "ok"}
status RGB LED Query {"onoff": power_state, "input": input_state}

power_state has the following possible values:

Value
on
off

input_state has the following possible values:

Value
off
optical
aux
line-in
bluetooth

Finish

With that we can now send HTTP commands to our Pico:

And it’s a win win because my remote control can go on its adventures into the deep recesses of the couch and I can still turn on my soundbar:

Embedding a Pi Zero in a NES controller