https://kennedn.com/blog/posts/snowdon/ [logo]Kennedn's Blog Home Hijacking infrared to make a dumb device smart Saturday, November 26, 2022 [tv_side] [tv_front] 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 + Plan of Attack * Decoding the Indicator LED + Wiring + Code + Demo * Mimicing an Infrared Remote + Wiring + Code + Demo * Building a dunderhead HTTP server from scratch + What even is HTTP anyway? + Code + Demo * Optional: Recycling unused pins for SWD debugging * Tying it all together + Wiring + Compiling Guide + API * Finish 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. [motherboard_front] [motherboard_front_closeup] 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 [snowdon_fr] 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 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 #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 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. [wokwi_badg] 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: [nec_protocol] 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 [snowdon_fr] 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: + Youtube - Raspberry Pi Pico's PIO + Youtube - Raspberry Pi Pico and RP2040 + PDF - Pico C SDK, Section 3 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). [nec_protocol_1] 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! [nec_protocol_2] We then start again from next until we have processed all 32 bits: [nec_protocol] 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 [nec_protocol_3] 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); } [wokwi_badg] 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 /api/v1.0 Specifies the sub-path target of the call, this Target /pc?code= allows routing in a HTTP server that hosts many status 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/ This is the HTTP version of the call, for our 1.1 purposes it will always be HTTP/1.1 Status 200 A numeric representation of the response status, must Code be a valid HTTP status code as documented here Status OK A textual representation of the response status, must Text 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 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 #include 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 [wokwi_badg] 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: [snowdon_mainboard_usb] 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: [panel_back] [panel_connectors] 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 [snowdon_fr] 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 [snowdon_fr] 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 [motherboard_back_wiring] [connected_with_daughter_board] [pico_closeup] [remount] 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 and with own values cmake -DPICO_BOARD=pico_w -DWIFI_SSID="" -DWIFI_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://: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: [snowdon_http_test] 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: [snowdon_watch] Embedding a Pi Zero in a NES controller (c) Joshua Kennedy 2022 Site code is open source [?] and can be found here