
Trekk i spaken, fyr av en request: Bygg en partyspake i Rust
Hva får du hvis du kombinerer en mikrokontroller, Rust, 3D printing og et API som styrer kontoret?
Svaret er Partyspaken, en 3D-printet spake som på få sekunder forvandler Snøkam-kontoret fra arbeidsmodus til full afterwork. 🍻 Dra spaken ned -> Sonos starter Snøkam Afterwork-spillelisten -> TV-en spiller av en "fredag-vibez"-video -> Philips hue lysene blir dempet -> Slack-melding som informerer om fest på kontoret -> kontoret går i festmodus.
Hva er Partyspaken?
Kort forklart, partyspaken er en 3D-printet spake, hvor vi bruker en ESP32 mikrokontroller til å sende HTTP-requests til vårt internsystem, som deretter bruker sine APIer til å skru på musikk, spille av videoer på Tven, sende slack-melding til alle om at nå er det fest på kontoret og dempe belysningen. 🕺
Hvordan vi bygde den
Mikrokontroller: ESP32
Vi valgte ESP32 som vår plattform fordi den kombinerer lav pris, innebygd Wi-Fi og gode tilkoblingsmuligheter. For et prosjekt som Partyspaken, hvor den bare skal kunne sende enkle HTTP-requests, gir den en riktige balansen mellom ytelse og enkelhet. Det er et mer “plug and play”-vennlig utgangspunkt og passer bra til et prosjekt som skal være både robust og raskt å realisere.
Det eksisterer andre plattformer, eksempelvis STM32 og Rasberry Pi Pico, men her får du vanligvis ikke wifi med på kjøpet, som gjør det mindre aktuelt for dette prosjektet.
Språkvalg: Rust på ESP32
Deretter måtte vi bestemme oss for språk, og etter at det hvite hus i 2024 kom ut og sa at "nå må vi omfavne minnesikre programmeringsspråk", så falt valget på Rust.
Rust har heldigvis et veldig godt økosystem for utvikling på ESP32 plattformen, med mye dokumentasjon og ressurser. Jeg vil sterkt anbefale ESP-RS sin dokumentasjon hvis man vurderer å prøve, da det gir en god introduksjon til hvordan sette opp utviklingsmiljøer, flashe din første ESP32 med mer.
Valget mellom std og no_std
I Rust kan du velge om du ønsker å bruke standardbiblioteket (std) eller gå for en minimal runtime med no_std. Begge har fordeler og ulemper, og valget handler ofte om balansen mellom funksjonalitet og ressursbruk.
std = mer funksjonalitet, mindre hodebry
Standardbiblioteket gir mange funksjoner rett ut av boksen, blant annet:
- Datastrukturer (Vec, HashMap, String, osv.)
- Filhåndtering og lagring.
- Nettverksstøtte (HTTP, TCP/UDP).
- Tråder og parallellitet
- Error handling og panics med std::panic.
En fordel med std er at mange populære crates bygger på standardbibliotekets funksjoner (nettverk, tråder, filsystem), så ting “bare virker”. no_std har etter hvert solide alternativer for embedded, men krever mer bevisste valg (ofte med default-features = false, alloc og dedikerte embedded-crates).
no_std = maksimal kontroll, minimal overhead
no_std er laget for miljøer med svært begrensede ressurser, typisk mikrokontrollere uten operativsystem, som ESP32 og STM32. Når du kjører no_std:
- Du dropper hele standardbiblioteket → programmet blir mindre og raskere å laste.
- Men: Du mister automatisk støtte for ting som tråder, filsystem og sockets.
- Mange crates fungerer ikke direkte → du må ofte finne alternativer som er skrevet for embedded, f.eks. embedded-hal.
Dette er typisk nødvendig hvis:
- Du jobber med ultra-low-power IoT-enheter.
- Du har svært begrenset flash/RAM.
Vårt valg for Partyspaken
I vårt tilfelle hadde vi ingen strenge begrensninger på minne, lagringsplass eller kompleksitet. Partyspaken kjører på en ESP32 med nok RAM og flash, den er koblet på strøm, og vi ønsket å:
- Bruke eksisterende crates for HTTP-requests, JSON-håndtering og async.
- Komme raskt i gang uten å bygge alt fra scratch.
Derfor valgte vi å gå for std. Det gir oss raskere utvikling, enklere kode og god støtte i Rust-økosystemet.
Hardware og 3D-modellering
Vi valgte en ESP32-WROOM-32D-modul fordi:
- Den har innebygd Wi-Fi.
- God støtte i Rust-økosystemet.
- Lav pris (~60 kr).
og sammen med et ESP32 tilkoblingsbrett så har du en plattform hvor det er relativt lett å dra kabler uten å måtte ta frem loddebolten.
For selve spaken kjøpte vi inn en faktisk spake, demonterte den slik at vi sto igjen med basen. Deretter brukte en av våre konsulenter Øystein Lasson FreeCAD til å 3d-modellere en ny spake som omkranset basen.
Til dette brukte vi en Prusa mk3s+ for å 3D-printe hele spaken i PLA.
Hvor vår ESP-32 er bygd inn i bunnen av spaken, med litt suboptimal kabling og elektrikerteip.
Koden: Under panseret
Under panseret er logikken veldig enkel. Først setter vi opp alt av I/O, og i dette tilfellet er det bare kontroll av led på mikrokontrolleren ved bruk av GPIO2(output) og lesing av spenning på GPIO4(input), hvor den er satt til å være Pull-up. Pull-up sørger for at GPIO4 leser en stabil HIGH når spaken er åpen, og LOW når spaken kobles til jord.
fn main() -> anyhow::Result<()> {esp_idf_svc::sys::link_patches();// Connects rusts !info / !error macros to ESP-idf log backendEspLogger::initialize_default();// Initialize ESP32 peripherals and system resourceslet peripherals = Peripherals::take()?;let sys_loop = EspSystemEventLoop::take()?;let timer_service = EspTaskTimerService::new()?;let nvs = EspDefaultNvsPartition::take()?;// Configure GPIO2 as output for LED indicatorlet mut led = PinDriver::output(peripherals.pins.gpio2).unwrap();// Configure GPIO4 as input for lever/switch with pull-up resistorlet mut lever = PinDriver::input(peripherals.pins.gpio4).unwrap();let _ = lever.set_pull(Pull::Up);
Neste steg er å få koblet seg på wifi på kontoret.
// Initialize WiFi with async support for non-blocking operationslet mut wifi = AsyncWifi::wrap(EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs))?,sys_loop,timer_service,)?;// WiFi connection loop with retry logicloop {match block_on(wifi_connect::connect_wifi(&mut wifi)) {Ok(()) => {info!("Wifi connected!");break;}Err(e) => {error!("Wifi connect failed: {:?}", e);std::thread::sleep(std::time::Duration::from_secs(5));info!("Retrying wifi connection...");}}}
const SSID: &str = env!("WIFI_SSID");const PASSWORD: &str = env!("WIFI_PASS");// Connects to WiFi networkpub async fn connect_wifi(wifi: &mut AsyncWifi<EspWifi<'static>>) -> anyhow::Result<()> {let wifi_configuration: Configuration = Configuration::Client(ClientConfiguration {ssid: SSID.try_into().unwrap(),bssid: None,auth_method: AuthMethod::WPA2Personal,password: PASSWORD.try_into().unwrap(),channel: None,..Default::default()});// Apply configuration to WiFi driverwifi.set_configuration(&wifi_configuration)?;wifi.start().await?;info!("Wifi started");info!("Connecting to SSID: {}", SSID);wifi.connect().await?;info!("Wifi connected");wifi.wait_netif_up().await?;info!("Wifi netif up");Ok(())}
Til slutt må man bare kombinere litt spake-logikk med en HTTP-request og voila.
// Create HTTP client oncelet http_config = http_client::HttpClientConfig::new();let http_client = http_client::HttpClient::new(http_config);// before the loop, read the initial state once:let mut prev = lever.is_high();loop {let curr = lever.is_high();if curr != prev {let _ = led.toggle();// Lever UP (GPIO HIGH) → party: false (no party)// Lever DOWN (GPIO LOW) → party: true (party time!)match http_client.send_party_payload(!curr) {Ok(reply) => info!("Payload OK: {}", reply),Err(err) => error!("Payload failed: {:?}", err),}prev = curr;}std::thread::sleep(core::time::Duration::from_millis(100));}
pub fn send_party_payload(&self, party: bool) -> anyhow::Result<String> {// Create HTTP client connectionlet mut client = self.make_client()?;// Serialize party state to JSONlet payload = SnokamParty { party: party };let body = serde_json::to_string(&payload)?;let len_str = body.len().to_string();// Prepare HTTP headerslet headers = &[("host", self.config.host.as_str()), // Target host("content-type", "application/json"), // JSON content type("content-length", len_str.as_str()), // Body length("authorization", self.config.authorization.as_str()), // API authorization("connection", "close"), // Close connection after request];// Create and submit HTTP PUT requestlet mut req = client.request(Method::Put,self.config.url.as_str(),headers,)?;// Write JSON payload to request bodyreq.write_all(body.as_bytes())?;// Submit request and get responselet mut resp = req.submit()?;// Read response body into bufferlet mut buf = vec![0u8; self.config.buffer_size];let n = resp.read(&mut buf)?;// Convert response to UTF-8 string, handling invalid UTF-8 gracefullylet resp_str = core::str::from_utf8(&buf[..n]).unwrap_or("<invalid utf8>").to_string();Ok(resp_str)}
I eksempelet over så er det mye forbedringspotensiale, det er for eksempel ikke implementert feilhåndtering, så hva skjer hvis esp'en mister nettet? Da er den litt lost, det kan fikses, men er utenfor scope i denne bloggposten.
Internsystemet: Det som kobler alt sammen
I Snøkam har vi et internsystem som håndterer det meste for oss. Det kan være alt fra planlegging av sosiale events, lønnskjøring, håndboka, webshop med mer. For et skikkelig dypdykk så anbefaler jeg bloggserien vår "Krystallklart" om temaet, som du kan finne her.
I dette internsystemet har vi også en microtjeneste som heter office-functions, som tilgjengeliggjør en rekke APIer som vi kan benytte oss av. Disse APIene styrer det meste av lyd, lys og video på kontoret. Dette har vi koblet sammen under et api, party, som spiller av en forhåndsbestemt video på Tven, musikk på sonosanlegget, og lys på ulike hues som vi har plassert rundt omkring. Det gjør det veldig enkelt å trigge hele systemet med en enkel request fra vår ESP32.
Resultatet
Fra arbeid til afterwork på sekunder
Spaken virker! Når vi drar i den, starter musikken, TV-en spiller av video, og Slack får beskjed om at festen er i gang.
Teknisk sett er det ikke mer magisk enn en ESP32 som sender en request til vårt API, men opplevelsen er større enn summen av delene. Vi har fått et fysisk symbol på overgangen fra jobb til sosialt, og en unnskyldning til å teste Rust på embedded hardware.
Det er fortsatt ting vi kan forbedret (kabling, feilhåndtering, mer lysstyring) og som ligger i todo-lista, men den gjør det den skal: ett rykk i spaken, og kontoret endrer modus. 🎉
Hvis du har lest helt hit, og kanskje fikk lyst på en partyspak på ditt eget kontor, ta kontakt på hei@snokam.no, så fikser vi noe!