Community project

Digital Rudder Heading Alarm

Photo of Digital Rudder Heading Alarm
Generated with AI

Basar Titiz

Published November 12, 2026

This digital rudder heading alarm helps boat operators maintain a set course by continuously monitoring heading with a 9-axis IMU and displaying it on a low-power e-ink screen. The system alerts the helmsman with a piezo buzzer when the vessel drifts beyond a user-defined heading tolerance, making it useful for autopilot-free navigation or as a backup heading monitor.

Builders will receive a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the display, IMU sensor, buzzer, and control buttons to an ESP32 microcontroller. The guide includes calibration procedures and land-based testing steps before deploying the alarm on a vessel, along with the full firmware source code for customizing alarm thresholds and behavior.

Wiring diagram

Wiring diagram for Digital Rudder Heading Alarm
Loading diagram…

Gather all the parts

QtyComponent
1

Waveshare 2.9inch E-Ink Display Module (296x128)

2.9 inch siyah-beyaz V2, SSD1680, 296x128

Waveshare 2.9-inch monochrome E-Ink/e-Paper display module, 296×128 resolution, SPI interface, 3.3V logic, supports full refresh and fast partial refresh. Compatible with ESP32 via hardware SPI (VSPI/HSPI). Driven by the GxEPD2 Arduino library with built-in partial-refresh support for RSVP-style word flipping. Shares SPI bus with other peripherals using a dedicated CS pin.

1

BNO055 9-Axis IMU

Bosch BNO055 intelligent 9-axis absolute orientation sensor. It combines accelerometer, gyroscope, magnetometer, and onboard sensor fusion, returning fused orientation data over I2C or UART. Common breakouts expose I2C plus optional address, interrupt, and reset pins.

1

Piezo Buzzer

Small passive piezo buzzer (TDK PS1240) for beeps, tones and alerts. It has no built-in oscillator, so drive one lead with a 3-30 V square wave from a microcontroller pin using tone() or PWM and connect the other lead to ground. It is loudest around 4 kHz and works from about 2 to 10 kHz; driving both leads in antiphase roughly doubles the volume.

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

Assemble it in 5 steps

1. Ekranı bağlayın

Güç kapalıyken Waveshare 2.9 inç siyah-beyaz V2 (SSD1680) ekran kullanın. VCC → 3V3, GND → GND. DIN → GPIO23 (veri), CLK → GPIO18 (saat), CS → GPIO27 (seçim), DC → GPIO26 (komut), RST → GPIO25 (sıfırlama), BUSY → GPIO33 (hazır durumu).

  • Üç renkli, V1 veya çıplak ekran paneli bu yazılımla uyumlu değildir.

2. Yön sensörünü bağlayın

Adafruit BNO055 modülünde VIN → 3V3, GND ve ADR → GND, SDA → GPIO21 (veri), SCL → GPIO22 (saat). INT ve RST boş kalır. Kısa kablo kullanın.

3. Düğmeleri ve sesi bağlayın

SET düğmesinin SIGNAL ucu GPIO13, eksi düğmesinin SIGNAL ucu GPIO14, artı düğmesinin SIGNAL ucu GPIO16 olur. Üçünün GND uçlarını GND’ye bağlayın. Pasif PS1240 piezonun SIG ucu GPIO32, diğer ucu GND olur. Dört bacaklı düğmede basıldığında birleşen karşı kontakları kullanın.

4. Tekneye sağlam ve kuru monte edin

Ekranı kürekçiye bakacak şekilde, sensörü ise tekneye sabit ve düz monte edin. Sensörü mıknatıs, çelik bağlantı, telefon, hoparlör ve güç kablolarından uzak tutun. Şeffaf kapaklı su geçirmez plastik kutu ve sızdırmaz kablo geçişleri kullanın; piezo için su almayan ses çıkışı bırakın. USB kablosunu kuru ve sabit tutun. Güç kartın USB girişinden sağlanır.

  • Açık devreyi suya maruz bırakmayın. Bu prototip tek başına seyir güvenliği aracı değildir.

5. Kalibre edip karada deneyin

Her açılışta sensörü yavaşça farklı yönlerde çevirin, sekiz çizin, altı yüzünde kısa süre sabit tutun. Kalibrasyon yazısı kaybolmadan hedef etkinleşmez. Son konumunda manyetik pusulayla 0/90/180/270 dereceleri karşılaştırın; sabit yön farkı varsa sensörün düzlemdeki montajını döndürüp düzeltin. Tekneyi istenen yöne çevirip SET ile o yönü alın veya artı/eksi ile hedefi birer derece ayarlayın. Sağ ve sol 4 derece sapmayı, 359/0 geçişini, eğimi ve sesi deneyin. Hedef her açılışta yeniden ayarlanır.

  • Ölçüm manyetik kuzeye göredir, GPS yer üzerindeki hareket yönü değildir. Sensörü kürekçinin baktığı yöne değil teknenin başına göre hizalayın.
  • 3 derece alarm eşiğidir; dalga, ivme ve manyetik etki altında 3 derece ölçüm doğruluğu garantisi değildir.
  • Normal sapma kısa tekrar eden bip, geçersiz ölçüm çift biptir. Ölçüm geçersizleşince hedef devreden çıkar; kalibrasyon sonrası SET ile yeniden alın.
  • E-kâğıt yaklaşık saniyede bir görsel güncelleme sağlar; ses kontrolü ekran yenilemesinden bağımsızdır. Güç kesilince son görüntü ekranda kalabilir.

Review all connections

1. Connections between "screen" and "ESP32"

FunctionscreenESP32
powerVCC3V3
groundGNDGND
spiDINGPIO 23
spiCLKGPIO 18
spiCSGPIO 27
digitalDCGPIO 26
digitalRSTGPIO 25
digitalBUSYGPIO 33

2. Connections between "compass" and "ESP32"

FunctioncompassESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22
groundADRGND

3. Connections between "sound" and "ESP32"

FunctionsoundESP32
pwmSIGGPIO 32
groundGNDGND

4. Connections between "set" and "ESP32"

FunctionsetESP32
digitalSIGNALGPIO 13
groundGNDGND

5. Connections between "minus" and "ESP32"

FunctionminusESP32
digitalSIGNALGPIO 14
groundGNDGND

6. Connections between "plus" and "ESP32"

FunctionplusESP32
digitalSIGNALGPIO 16
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_BNO055.h>
#include <GxEPD2_BW.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <math.h>
Adafruit_BNO055 bno(55,0x28);
GxEPD2_BW<GxEPD2_290_T94_V2,GxEPD2_290_T94_V2::HEIGHT> display(GxEPD2_290_T94_V2(27,26,25,33));
struct Frame { int heading,target,error,state; };
QueueHandle_t frames;
bool sensor=false,armed=false; float target=0;
float wrap(float a){a=fmodf(a+360,360);return a<0?a+360:a;}
float difference(float a,float b){return wrap(a-b+180)-180;}
struct Button {int pin;bool raw=true,stable=true;uint32_t changed=0;
 bool pressed(){bool v=digitalRead(pin);if(v!=raw){raw=v;changed=millis();}if(v!=stable && millis()-changed>=35){stable=v;return !v;}return false;}
};
Button buttons[]={{13},{14},{16}};
void screenTask(void*){
 SPI.begin(18,19,23,27); display.init(115200,true,2,false); display.setRotation(1); display.setTextColor(GxEPD_BLACK); display.setTextWrap(false);
 Frame f,last={-999,-999,-999,-999}; unsigned updates=0;
 for(;;){
 if(xQueueReceive(frames,&f,portMAX_DELAY)!=pdTRUE)continue;
 if(f.heading==last.heading&&f.target==last.target&&f.error==last.error&&f.state==last.state)continue;
 if(updates++%60==0)display.setFullWindow();else display.setPartialWindow(0,0,296,128);
 display.firstPage();do{
 display.fillScreen(GxEPD_WHITE);display.setTextSize(2);display.setCursor(4,3);display.print("TEKNE YONU");
 display.setCursor(4,25);display.setTextSize(5);
 if(f.state<2)display.print("---");else {char s[8];snprintf(s,sizeof(s),"%03d",f.heading);display.print(s);} display.setTextSize(2);display.print(" deg");
 display.setCursor(4,70);display.printf("HEDEF %03d  FARK %+d",f.target,f.error);
 display.setCursor(4,96);
 const char* text=f.state==0?"SENSOR HATASI":f.state==1?"KALIBRASYON GEREK":f.state==2?"SET: HEDEF KAYDET":f.state==4?"SAPMA >3 DERECE":"ROTA TAMAM";
 display.print(text);
 }while(display.nextPage());display.powerOff();last=f;
 vTaskDelay(pdMS_TO_TICKS(500));
 }
}
void setup(){
 Serial.begin(115200); for(auto &b:buttons)pinMode(b.pin,INPUT_PULLUP);pinMode(32,OUTPUT);noTone(32);
 Wire.begin(21,22);Wire.setClock(100000);Wire.setTimeOut(100);
 sensor=bno.begin();frames=xQueueCreate(1,sizeof(Frame));
 xTaskCreate(screenTask,"screen",6144,nullptr,1,nullptr);
}
void loop(){
 bool press[3];for(int i=0;i<3;i++)press[i]=buttons[i].pressed();
 uint8_t sys=0,g=0,a=0,m=0,status=0,test=0,err=0; float h=0;
 if(sensor){bno.getCalibration(&sys,&g,&a,&m);bno.getSystemStatus(&status,&test,&err);h=bno.getVector(Adafruit_BNO055::VECTOR_EULER).x();}
 bool valid=sensor&&status==5&&err==0&&sys==3&&g==3&&a==3&&m==3&&isfinite(h);
 if(!valid)armed=false;
 if(valid&&press[0]){target=wrap(h);armed=true;}
 if(valid&&press[1]){target=wrap(target-1);armed=true;}
 if(valid&&press[2]){target=wrap(target+1);armed=true;}
 float delta=difference(h,target);bool alarm=valid&&armed&&fabsf(delta)>3.0f;
 static bool sounding=false;bool beep=alarm&&(millis()%800<180);
 // Invalid readings use a distinct double chirp, never a misleading 'on course' silence.
 if(!valid)beep=(millis()%2500<100)||(millis()%2500>=200&&millis()%2500<300);
 if(beep!=sounding){if(beep)tone(32,4000);else noTone(32);sounding=beep;}
 Frame f={(int)lroundf(wrap(h))%360,(int)lroundf(target)%360,(int)lroundf(delta),!sensor||err?0:!valid?1:!armed?2:alarm?4:3};
 xQueueOverwrite(frames,&f);delay(50);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik