@@ -29,19 +29,22 @@ Then reboot or unload the modules manually:
2929
3030 sudo rmmod hts221_i2c hts221 lps25_i2c lps25 st_pressure st_sensors_i2c st_sensors
3131
32- # LED Matrix and Joystick Conflicts
32+ # LED Matrix and Joystick
3333
3434The LED matrix and joystick are controlled by an ATTINY88 at I2C address 0x46.
35- The kernel's rpisense-fb framebuffer driver claims this device by default .
35+ The kernel's rpisense-fb framebuffer driver exposes this as /dev/fb1 .
3636
37- To use the LED functions, also blacklist the framebuffer driver:
37+ The LED functions in this package use the framebuffer device, so ensure the
38+ rpisense_fb module is loaded (it is by default on Raspberry Pi OS):
3839
39- blacklist rpisense_fb
40- blacklist rpisense_js
40+ lsmod | grep rpisense
4141
42- Then reboot or unload :
42+ If running in Docker, expose the framebuffer device and sysfs :
4343
44- sudo rmmod rpisense_fb rpisense_js
44+ devices:
45+ - /dev/fb1:/dev/fb1
46+ volumes:
47+ - /sys/class/graphics:/sys/class/graphics:ro
4548
4649*/
4750
@@ -90,13 +93,14 @@ const (
9093
9194// SenseHat represents a Raspberry Pi Sense HAT device.
9295type SenseHat struct {
93- i2cFile * os.File
94- hasColorSensor bool
96+ i2cFile * os.File
97+ fbFile * os.File // LED matrix framebuffer
98+ hasColorSensor bool
9599 colorSensorAddr uint8 // I2C address of detected color sensor (0x29 or 0x39)
96- joystickFile * os.File
97- joystickEvents []JoystickEvent
98- joystickMu sync.Mutex
99- joystickDone chan struct {} // signals joystick goroutine to stop
100+ joystickFile * os.File
101+ joystickEvents []JoystickEvent
102+ joystickMu sync.Mutex
103+ joystickDone chan struct {} // signals joystick goroutine to stop
100104
101105 // HTS221 calibration coefficients
102106 h0RH , h1RH float64
@@ -159,6 +163,12 @@ func New() (*SenseHat, error) {
159163 // Try to initialize color sensor (Sense HAT v2 only)
160164 hat .hasColorSensor = hat .initColorSensor () == nil
161165
166+ // Initialize LED framebuffer
167+ if err := hat .initFramebuffer (); err != nil {
168+ f .Close ()
169+ return nil , fmt .Errorf ("failed to initialize LED framebuffer: %w" , err )
170+ }
171+
162172 // Initialize joystick
163173 hat .initJoystick ()
164174
@@ -174,6 +184,9 @@ func (s *SenseHat) Close() error {
174184 if s .joystickFile != nil {
175185 s .joystickFile .Close ()
176186 }
187+ if s .fbFile != nil {
188+ s .fbFile .Close ()
189+ }
177190 return s .i2cFile .Close ()
178191}
179192
@@ -669,43 +682,68 @@ func GetCPUTemperature() (float64, error) {
669682 return 0 , scanner .Err ()
670683}
671684
685+ // initFramebuffer finds and opens the Sense HAT LED framebuffer device.
686+ func (s * SenseHat ) initFramebuffer () error {
687+ // Find the framebuffer device with name "RPi-Sense FB"
688+ for i := 0 ; i < 10 ; i ++ {
689+ namePath := fmt .Sprintf ("/sys/class/graphics/fb%d/name" , i )
690+ name , err := os .ReadFile (namePath )
691+ if err != nil {
692+ continue
693+ }
694+ if strings .TrimSpace (string (name )) == "RPi-Sense FB" {
695+ fbPath := fmt .Sprintf ("/dev/fb%d" , i )
696+ fb , err := os .OpenFile (fbPath , os .O_RDWR , 0 )
697+ if err != nil {
698+ return fmt .Errorf ("failed to open framebuffer %s: %w" , fbPath , err )
699+ }
700+ s .fbFile = fb
701+ return nil
702+ }
703+ }
704+ return fmt .Errorf ("Sense HAT framebuffer not found" )
705+ }
706+
672707// ClearLEDs turns off all LEDs on the 8x8 matrix.
673708func (s * SenseHat ) ClearLEDs () error {
674- if err := setI2CAddr (s .i2cFile , addrLEDMatrix ); err != nil {
709+ if s .fbFile == nil {
710+ return fmt .Errorf ("framebuffer not initialized" )
711+ }
712+ // Write 128 bytes of zeros (64 pixels × 2 bytes RGB565)
713+ buf := make ([]byte , 128 )
714+ if _ , err := s .fbFile .Seek (0 , 0 ); err != nil {
675715 return err
676716 }
677- // Write 129 bytes: 1 register address + 128 bytes of zeros (64 pixels × 2 bytes RGB565)
678- buf := make ([]byte , 129 )
679- buf [0 ] = 0x00 // Register address
680- _ , err := s .i2cFile .Write (buf )
717+ _ , err := s .fbFile .Write (buf )
681718 return err
682719}
683720
684721// SetPixel sets a single pixel on the 8x8 LED matrix.
685- // x and y are 0-7, r/g/b are 0-255.
686- // Uses RGB565 format: 5 bits red, 6 bits green, 5 bits blue packed into 2 bytes .
722+ // x (column) and y (row) are 0-7, r/g/b are 0-255.
723+ // Uses RGB565 format via framebuffer .
687724func (s * SenseHat ) SetPixel (x , y int , r , g , b uint8 ) error {
688725 if x < 0 || x > 7 || y < 0 || y > 7 {
689726 return fmt .Errorf ("pixel coordinates out of range: (%d, %d)" , x , y )
690727 }
691- if err := setI2CAddr ( s . i2cFile , addrLEDMatrix ); err ! = nil {
692- return err
728+ if s . fbFile = = nil {
729+ return fmt . Errorf ( "framebuffer not initialized" )
693730 }
694- // Pixel offset in framebuffer: row-major order, 2 bytes per pixel (RGB565)
695- // y = row, x = column
696- offset := (y * 8 + x ) * 2
697- // Pack RGB into RGB565: (r >> 3) << 11 | (g >> 2) << 5 | (b >> 3)
698- r5 := uint16 (r >> 3 )
699- g6 := uint16 (g >> 2 )
700- b5 := uint16 (b >> 3 )
731+ // Pixel offset: row-major order, 2 bytes per pixel
732+ offset := int64 ((y * 8 + x ) * 2 )
733+ // Pack RGB into RGB565: 5 bits red, 6 bits green, 5 bits blue
734+ r5 := uint16 (r >> 3 ) & 0x1F
735+ g6 := uint16 (g >> 2 ) & 0x3F
736+ b5 := uint16 (b >> 3 ) & 0x1F
701737 rgb565 := (r5 << 11 ) | (g6 << 5 ) | b5
702- // Write register address followed by 2-byte RGB565 value (little-endian )
738+ // Write as little-endian 16-bit value (struct.pack('H', ...) in Python )
703739 buf := []byte {
704- byte (offset ),
705- byte (rgb565 & 0xFF ), // low byte
706- byte ((rgb565 >> 8 ) & 0xFF ), // high byte
740+ byte (rgb565 & 0xFF ),
741+ byte ((rgb565 >> 8 ) & 0xFF ),
742+ }
743+ if _ , err := s .fbFile .Seek (offset , 0 ); err != nil {
744+ return err
707745 }
708- _ , err := s .i2cFile .Write (buf )
746+ _ , err := s .fbFile .Write (buf )
709747 return err
710748}
711749
0 commit comments