Skip to content

Commit a1657d3

Browse files
feat: add Sense HAT integration module
1 parent fdac92d commit a1657d3

File tree

6 files changed

+597
-0
lines changed

6 files changed

+597
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Thanks to: @Crazylegstoo, @dathbe, @m-idler, @plebcity, @khassel, @KristjanESPER
6969
- Add configuration option for `User-Agent`, used by calendar & news module (#3255)
7070
- [linter] Add prettier plugin for nunjuck templates (#3887)
7171
- [core] Add clear log for occupied port at startup (#3890)
72+
- [sensehat] Sense HAT integration module (`sensehat`) to display temperature, humidity, pressure, optional orientation, and to control the LED matrix on Raspberry Pi.
7273

7374
### Changed
7475

config/config.js.sample

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,32 @@ let config = {
104104
broadcastNewsUpdates: true
105105
}
106106
},
107+
// Example Sense HAT module configuration.
108+
// Uncomment this block on a Raspberry Pi with a Sense HAT installed:
109+
// {
110+
// module: "sensehat",
111+
// position: "top_right",
112+
// config: {
113+
// updateInterval: 5000,
114+
// showTemperature: true,
115+
// showHumidity: true,
116+
// showPressure: true,
117+
// showOrientation: false,
118+
// temperatureUnit: "C",
119+
// roundValues: 1,
120+
// ledMatrixEnabled: true,
121+
// ledMode: "status", // "off" | "status" | "text"
122+
// ledText: "Hello from Sense HAT",
123+
// ledColor: [0, 255, 0],
124+
// criticalThresholds: {
125+
// temperatureHigh: 30,
126+
// temperatureLow: 10,
127+
// humidityHigh: 80,
128+
// humidityLow: 20
129+
// },
130+
// debug: false
131+
// }
132+
// },
107133
]
108134
};
109135

modules/default/sensehat/README.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
Sense HAT Module for MagicMirror²
2+
3+
This module integrates the Raspberry Pi Sense HAT with MagicMirror². It reads sensor data and can control the 8×8 LED matrix for simple status indication or text display.
4+
5+
Features
6+
7+
- Display temperature, humidity, pressure, and optionally orientation (pitch/roll/yaw)
8+
- Periodic polling interval configurable
9+
- Optional LED matrix control: off, status color, or scrolling text
10+
- Simple threshold-based LED status (green/normal, red/out-of-range)
11+
12+
Requirements
13+
14+
- Raspberry Pi with an attached Sense HAT
15+
- Python 3 and the official Sense HAT library
16+
17+
Install dependencies on the Raspberry Pi:
18+
sudo apt update
19+
sudo apt install -y sense-hat python3-sense-hat
20+
21+
Installation
22+
23+
The module lives in this repository under `modules/default/sensehat`.
24+
25+
On a real Raspberry Pi installation of MagicMirror, you can deploy it in one of two ways:
26+
27+
1. Keep it under `modules/default/sensehat` and create a symlink so MagicMirror can also find it as `modules/sensehat`:
28+
- cd ~/MagicMirror/modules
29+
- ln -s default/sensehat sensehat
30+
31+
2. Alternatively, place the module directly under `modules/sensehat` (preferred by some maintainers for non-core modules).
32+
33+
Additionally, ensure the Python helper is executable (optional):
34+
35+
- chmod +x modules/default/sensehat/python/reader.py
36+
37+
### Raspberry Pi deployment example
38+
39+
Assuming your MagicMirror installation is in `~/MagicMirror`:
40+
41+
```
42+
cd ~/MagicMirror/modules
43+
# if the module is under modules/default/sensehat, create a symlink
44+
ln -s default/sensehat sensehat
45+
```
46+
47+
Configuration
48+
Add the module to your config/config.js:
49+
50+
{
51+
module: "sensehat",
52+
position: "top_right",
53+
config: {
54+
updateInterval: 5000,
55+
showTemperature: true,
56+
showHumidity: true,
57+
showPressure: true,
58+
showOrientation: false,
59+
temperatureUnit: "C",
60+
roundValues: 1,
61+
ledMatrixEnabled: true,
62+
ledMode: "status", // "off" | "status" | "text"
63+
ledText: "Hello from Sense HAT",
64+
ledColor: [0, 255, 0],
65+
criticalThresholds: {
66+
temperatureHigh: 30,
67+
temperatureLow: 10,
68+
humidityHigh: 80,
69+
humidityLow: 20
70+
},
71+
debug: false
72+
}
73+
}
74+
75+
How it works
76+
77+
- Frontend (sensehat.js): Displays data and optionally sends LED status commands based on thresholds.
78+
- Node Helper (node_helper.js): Spawns the Python helper to read sensors at a set interval and forwards LED commands to it.
79+
- Python Helper (python/reader.py): Uses from sense_hat import SenseHat to read sensors and control the LED matrix. Outputs JSON to stdout in read mode.
80+
81+
Troubleshooting
82+
83+
1. Check that the Sense HAT is detected by the kernel
84+
85+
```
86+
ls -l /dev/i2c*
87+
dmesg | grep -i "sense"
88+
```
89+
90+
You should see:
91+
92+
- /dev/i2c-1 present
93+
- A line like: fb1: RPi-Sense FB frame buffer device
94+
- A joystick device entry for the Sense HAT
95+
96+
2. Probe I²C bus 1
97+
98+
```
99+
sudo i2cdetect -y 1
100+
```
101+
102+
On a working Sense HAT, you should see several non-"--" addresses (e.g. 1c, 39, 5c, 5f, 6a). If everything shows "--", the HAT may not be seated correctly or could be faulty.
103+
104+
3. Test using the official Python library
105+
106+
```
107+
python3 - << 'PY'
108+
from sense_hat import SenseHat
109+
110+
sh = SenseHat()
111+
112+
print("Temperature:", sh.get_temperature())
113+
print("Humidity :", sh.get_humidity())
114+
print("Pressure :", sh.get_pressure())
115+
PY
116+
```
117+
118+
- If you get numeric values, the sensors are working.
119+
- If you see errors like `OSError: Humidity Init Failed`, there may be a contact problem on the header or a sensor issue.
120+
121+
4. Check the LED matrix
122+
123+
```
124+
python3 - << 'PY'
125+
from sense_hat import SenseHat
126+
from time import sleep
127+
128+
sh = SenseHat()
129+
sh.clear()
130+
sh.show_message("HI", text_colour=(0, 255, 0), scroll_speed=0.07)
131+
sleep(1)
132+
sh.clear()
133+
PY
134+
```
135+
136+
If you don’t see LEDs:
137+
138+
- Power off the Raspberry Pi.
139+
- Firmly press the Sense HAT onto the 40-pin header (common on new boards).
140+
- Boot again and re-run the test.
141+
142+
5. What the MagicMirror module will show
143+
144+
- "Loading Sense HAT data…" → Python helper hasn’t delivered any data yet.
145+
- "Sense HAT: no sensor data (check hardware or drivers)" → helper runs, but all sensor fields are null.
146+
- "Sense HAT error: …" → helper reported an explicit error (library missing, init failure, etc.).
147+
148+
If the Python tests fail, the issue is with hardware/OS/driver rather than this MagicMirror module.
149+
150+
License
151+
MIT (follow the MagicMirror² project license)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
const { spawn } = require("child_process");
2+
const path = require("path");
3+
const NodeHelper = require("node_helper");
4+
const Log = require("logger");
5+
6+
module.exports = NodeHelper.create({
7+
start () {
8+
this.config = null;
9+
this.updateTimer = null;
10+
this.pythonPath = "python3"; // most Pis use python3
11+
this.readerPath = path.join(__dirname, "python", "reader.py");
12+
Log.info("[sensehat] Node helper started");
13+
},
14+
15+
stop () {
16+
if (this.updateTimer) {
17+
clearInterval(this.updateTimer);
18+
this.updateTimer = null;
19+
}
20+
},
21+
22+
socketNotificationReceived (notification, payload) {
23+
if (notification === "SENSEHAT_CONFIG") {
24+
this.config = payload || {};
25+
Log.info("[sensehat] Configuration received");
26+
this._setupPolling();
27+
28+
// optional: set initial LED state
29+
if (this.config.ledMatrixEnabled) {
30+
this._handleLedCommand({ mode: this.config.ledMode || "status", color: this.config.ledColor || [0, 255, 0], text: this.config.ledText || "" });
31+
}
32+
} else if (notification === "SENSEHAT_LED_COMMAND") {
33+
this._handleLedCommand(payload || {});
34+
}
35+
},
36+
37+
_setupPolling () {
38+
if (this.updateTimer) {
39+
clearInterval(this.updateTimer);
40+
this.updateTimer = null;
41+
}
42+
const interval = Math.max(1000, parseInt(this.config.updateInterval || 5000, 10));
43+
// Poll immediately, then on interval
44+
this._pollOnce();
45+
this.updateTimer = setInterval(() => this._pollOnce(), interval);
46+
Log.info(`[sensehat] Polling every ${interval} ms`);
47+
},
48+
49+
_pollOnce () {
50+
const args = [this.readerPath, "--read"]; // explicit --read
51+
const child = spawn(this.pythonPath, args, { cwd: path.dirname(this.readerPath) });
52+
53+
let stdout = "";
54+
let stderr = "";
55+
child.stdout.on("data", (d) => (stdout += d.toString()));
56+
child.stderr.on("data", (d) => (stderr += d.toString()));
57+
child.on("error", (err) => {
58+
Log.error(`[sensehat] Failed to spawn Python: ${err.message}`);
59+
});
60+
61+
child.on("close", (code) => {
62+
if (stderr && (this.config && this.config.debug)) {
63+
Log.warn(`[sensehat] python stderr: ${stderr.trim()}`);
64+
}
65+
if (code !== 0 && !stdout) {
66+
Log.warn(`[sensehat] Python exited with code ${code}`);
67+
return;
68+
}
69+
try {
70+
const data = JSON.parse(stdout.trim());
71+
if (data && data.error) {
72+
Log.warn(`[sensehat] Python reported error: ${data.error}`);
73+
// Forward error payload to frontend so UI can display it
74+
this.sendSocketNotification("SENSEHAT_DATA", data);
75+
return;
76+
}
77+
this.sendSocketNotification("SENSEHAT_DATA", data);
78+
} catch (e) {
79+
Log.warn(`[sensehat] Invalid JSON from python: ${e.message}. Raw: ${stdout.trim()}`);
80+
}
81+
});
82+
},
83+
84+
_handleLedCommand (cmd) {
85+
if (!this.config || !this.config.ledMatrixEnabled) return;
86+
87+
const args = [this.readerPath];
88+
if (cmd.clear || cmd.mode === "off") {
89+
args.push("--clear");
90+
} else if (cmd.mode === "text") {
91+
args.push("--mode", "text");
92+
if (cmd.text) {
93+
args.push("--text", String(cmd.text));
94+
}
95+
const color = Array.isArray(cmd.color) ? cmd.color : this.config.ledColor || [255, 255, 255];
96+
args.push("--color", color.join(","));
97+
} else {
98+
// status mode
99+
args.push("--mode", "status");
100+
const color = Array.isArray(cmd.color) ? cmd.color : this.config.ledColor || [0, 255, 0];
101+
args.push("--color", color.join(","));
102+
}
103+
104+
const child = spawn(this.pythonPath, args, { cwd: path.dirname(this.readerPath) });
105+
let stderr = "";
106+
child.stderr.on("data", (d) => (stderr += d.toString()));
107+
child.on("close", (code) => {
108+
if (code !== 0) {
109+
Log.warn(`[sensehat] LED command failed with code ${code}. ${stderr.trim()}`);
110+
} else if (this.config && this.config.debug && stderr.trim()) {
111+
Log.warn(`[sensehat] LED command stderr: ${stderr.trim()}`);
112+
}
113+
});
114+
}
115+
});

0 commit comments

Comments
 (0)