Skip to content

Instantly share code, notes, and snippets.

@robeden
Last active January 7, 2024 21:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robeden/fb61e395743b668e447c768dc5b1dab4 to your computer and use it in GitHub Desktop.
Save robeden/fb61e395743b668e447c768dc5b1dab4 to your computer and use it in GitHub Desktop.
Wifi Busy Light With Automatic Activation

The starting point for the project is the $5 WiFi Busy Light (formerly: Tiny Wifi Busy Light) project on hackaday.io. All credit to Jon for his excellent project.

For parts I ordered:

The case was printed from the designs with the original project, though they could use just a smidge more room behind the NeoPixels. I ended up using a file to make a bit more room.

Arduino

The board is based on the ESP8266, so support for that board needs to be installed to use the Arduino IDE. In addition, support for the Adafruit NeoPixel needs to be enabled in the Library Manager.

Note to self: this modification was required to fix a probem with pyserial on Big Sur.

Source is contained in BusyLight.ino. Wifi ID and password will need to be entered prior to upload.

Once the device is configured, it will respond to HTTP queries with a param of pin set to things like:

  • RED
  • GREEN
  • BLUE
  • OFF
  • ORANGE (this comes across as more yellow than YELLOW to me)
  • RBC (rainbow cycle) Example: http://192.168.1.201/?pin=RED

Micro Snitch

For tracking when the microphone or camera are active, I'm using Micro Snitch from Objective Development. I can log entries to a file that look like this:

Feb 20, 2021 at 8:15:33 PM: Activity log enabled
Feb 20, 2021 at 8:15:34 PM: Audio Device found: MacBook Pro Microphone – MacBook Pro Microphone
Feb 20, 2021 at 8:15:34 PM: Video Device found: FaceTime HD Camera
Feb 20, 2021 at 8:26:58 PM: Audio Device became active: MacBook Pro Microphone – MacBook Pro Microphone
Feb 20, 2021 at 8:27:20 PM: Video Device became active: FaceTime HD Camera
Feb 20, 2021 at 8:27:21 PM: Video Device became inactive: FaceTime HD Camera
Feb 20, 2021 at 8:27:32 PM: Audio Device became inactive: MacBook Pro Microphone – MacBook Pro Microphone

xBar

There are many ways to trigger a script to check the log, but mine is using xBar (formerly BitBar), which I already use for other things and will allow me to both run the check periodically and show in the menu bar what is being shown on the light.

The code for the xBar plugin will follow in a separate file. Note that I include source from the file_read_backwards python library (MIT license). Thanks to RobinNil for that work.

#!/usr/bin/python3
import io
import os
import sys
from typing import Optional, Tuple
import requests
LIGHT_ADDRESS = None # ex: "192.168.87.201"
COMMAND_FILE_LOCATION = "/Users/rob.eden/Library/Mobile Documents/com~apple~CloudDocs/Light Color/command"
COMMAND_FILE_MODE = "write" # "write", "read" or None
# NOTE: Most of this is support code from `file_read_backwards`.
# Interesting stuff is at the bottom.
##############################################################################################################
# https://github.com/RobinNil/file_read_backwards
# buffer_work_space.py
#
new_lines = ["\r\n", "\n", "\r"]
new_lines_bytes = [n.encode("ascii") for n in new_lines] # we only support encodings that's backward compat with ascii
class BufferWorkSpace:
"""It is a helper module for FileReadBackwards."""
def __init__(self, fp, chunk_size):
"""Convention for the data.
When read_buffer is not None, it represents contents of the file from `read_position` onwards
that has not been processed/returned.
read_position represents the file pointer position that has been read into read_buffer
initialized to be just past the end of file.
"""
self.fp = fp
self.read_position = _get_file_size(self.fp) # set the previously read position to the
self.read_buffer = None
self.chunk_size = chunk_size
def add_to_buffer(self, content, read_position):
"""Add additional bytes content as read from the read_position.
Args:
content (bytes): data to be added to buffer working BufferWorkSpac.
read_position (int): where in the file pointer the data was read from.
"""
self.read_position = read_position
if self.read_buffer is None:
self.read_buffer = content
else:
self.read_buffer = content + self.read_buffer
def yieldable(self):
"""Return True if there is a line that the buffer can return, False otherwise."""
if self.read_buffer is None:
return False
t = _remove_trailing_new_line(self.read_buffer)
n = _find_furthest_new_line(t)
if n >= 0:
return True
# we have read in entire file and have some unprocessed lines
if self.read_position == 0 and self.read_buffer is not None:
return True
return False
def return_line(self):
"""Return a new line if it is available.
Precondition: self.yieldable() must be True
"""
assert(self.yieldable())
t = _remove_trailing_new_line(self.read_buffer)
i = _find_furthest_new_line(t)
if i >= 0:
l = i + 1
after_new_line = slice(l, None)
up_to_include_new_line = slice(0, l)
r = t[after_new_line]
self.read_buffer = t[up_to_include_new_line]
else: # the case where we have read in entire file and at the "last" line
r = t
self.read_buffer = None
return r
def read_until_yieldable(self):
"""Read in additional chunks until it is yieldable."""
while not self.yieldable():
read_content, read_position = _get_next_chunk(self.fp, self.read_position, self.chunk_size)
self.add_to_buffer(read_content, read_position)
def has_returned_every_line(self):
"""Return True if every single line in the file has been returned, False otherwise."""
if self.read_position == 0 and self.read_buffer is None:
return True
return False
def _get_file_size(fp):
return os.fstat(fp.fileno()).st_size
def _get_next_chunk(fp, previously_read_position, chunk_size):
"""Return next chunk of data that we would from the file pointer.
Args:
fp: file-like object
previously_read_position: file pointer position that we have read from
chunk_size: desired read chunk_size
Returns:
(bytestring, int): data that has been read in, the file pointer position where the data has been read from
"""
seek_position, read_size = _get_what_to_read_next(fp, previously_read_position, chunk_size)
fp.seek(seek_position)
read_content = fp.read(read_size)
read_position = seek_position
return read_content, read_position
def _get_what_to_read_next(fp, previously_read_position, chunk_size):
"""Return information on which file pointer position to read from and how many bytes.
Args:
fp
past_read_positon (int): The file pointer position that has been read previously
chunk_size(int): ideal io chunk_size
Returns:
(int, int): The next seek position, how many bytes to read next
"""
seek_position = max(previously_read_position - chunk_size, 0)
read_size = chunk_size
# examples: say, our new_lines are potentially "\r\n", "\n", "\r"
# find a reading point where it is not "\n", rewind further if necessary
# if we have "\r\n" and we read in "\n",
# the next iteration would treat "\r" as a different new line.
# Q: why don't I just check if it is b"\n", but use a function ?
# A: so that we can potentially expand this into generic sets of separators, later on.
while seek_position > 0:
fp.seek(seek_position)
if _is_partially_read_new_line(fp.read(1)):
seek_position -= 1
read_size += 1 # as we rewind further, let's make sure we read more to compensate
else:
break
# take care of special case when we are back to the beginnin of the file
read_size = min(previously_read_position - seek_position, read_size)
return seek_position, read_size
def _remove_trailing_new_line(l):
"""Remove a single instance of new line at the end of l if it exists.
Returns:
bytestring
"""
# replace only 1 instance of newline
# match longest line first (hence the reverse=True), we want to match "\r\n" rather than "\n" if we can
for n in sorted(new_lines_bytes, key=lambda x: len(x), reverse=True):
if l.endswith(n):
remove_new_line = slice(None, -len(n))
return l[remove_new_line]
return l
def _find_furthest_new_line(read_buffer):
"""Return -1 if read_buffer does not contain new line otherwise the position of the rightmost newline.
Args:
read_buffer (bytestring)
Returns:
int: The right most position of new line character in read_buffer if found, else -1
"""
new_line_positions = [read_buffer.rfind(n) for n in new_lines_bytes]
return max(new_line_positions)
def _is_partially_read_new_line(b):
"""Return True when b is part of a new line separator found at index >= 1, False otherwise.
Args:
b (bytestring)
Returns:
bool
"""
for n in new_lines_bytes:
if n.find(b) >= 1:
return True
return False
##
##############################################################################################################
# https://github.com/RobinNil/file_read_backwards
# file_read_backwards.py
#
supported_encodings = ["utf-8", "ascii", "latin-1"] # any encodings that are backward compatible with ascii should work
class FileReadBackwards:
"""Class definition for `FileReadBackwards`.
A `FileReadBackwards` will spawn a `FileReadBackwardsIterator` and keep an opened file handler.
It can be used as a Context Manager. If done so, when exited, it will close its file handler.
In any mode, `close()` can be called to close the file handler..
"""
def __init__(self, path, encoding="utf-8", chunk_size=io.DEFAULT_BUFFER_SIZE):
"""Constructor for FileReadBackwards.
Args:
path: Path to the file to be read
encoding (str): Encoding
chunk_size (int): How many bytes to read at a time
"""
if encoding.lower() not in supported_encodings:
error_message = "{0} encoding was not supported/tested.".format(encoding)
error_message += "Supported encodings are '{0}'".format(",".join(supported_encodings))
raise NotImplementedError(error_message)
self.path = path
self.encoding = encoding.lower()
self.chunk_size = chunk_size
self.iterator = FileReadBackwardsIterator(io.open(self.path, mode="rb"), self.encoding, self.chunk_size)
def __iter__(self):
"""Return its iterator."""
return self.iterator
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Closes all opened its file handler and propagates all exceptions on exit."""
self.close()
return False
def close(self):
"""Closes all opened it s file handler."""
self.iterator.close()
def readline(self):
"""Return a line content (with a trailing newline) if there are content. Return '' otherwise."""
try:
r = next(self.iterator) + os.linesep
return r
except StopIteration:
return ""
class FileReadBackwardsIterator:
"""Iterator for `FileReadBackwards`.
This will read backwards line by line a file. It holds an opened file handler.
"""
def __init__(self, fp, encoding, chunk_size):
"""Constructor for FileReadBackwardsIterator
Args:
fp (File): A file that we wish to start reading backwards from
encoding (str): Encoding of the file
chunk_size (int): How many bytes to read at a time
"""
self.path = fp.name
self.encoding = encoding
self.chunk_size = chunk_size
self.__fp = fp
self.__buf = BufferWorkSpace(self.__fp, self.chunk_size)
def __iter__(self):
return self
def next(self):
"""Returns unicode string from the last line until the beginning of file.
Gets exhausted if::
* already reached the beginning of the file on previous iteration
* the file got closed
When it gets exhausted, it closes the file handler.
"""
# Using binary mode, because some encodings such as "utf-8" use variable number of
# bytes to encode different Unicode points.
# Without using binary mode, we would probably need to understand each encoding more
# and do the seek operations to find the proper boundary before issuing read
if self.closed:
raise StopIteration
if self.__buf.has_returned_every_line():
self.close()
raise StopIteration
self.__buf.read_until_yieldable()
r = self.__buf.return_line()
return r.decode(self.encoding)
__next__ = next
@property
def closed(self):
"""The status of the file handler.
:return: True if the file handler is still opened. False otherwise.
"""
return self.__fp.closed
def close(self):
"""Closes the file handler."""
self.__fp.close()
##
##############################################################################################################
def utf_to_stdout(text: str):
sys.stdout.buffer.write(text.encode("utf-8"))
sys.stdout.buffer.write("\n".encode("utf-8"))
sys.stdout.buffer.flush()
def is_mic_video_active() -> Tuple[bool, bool]:
line: str
audio_state = None
video_state = None
with FileReadBackwards(os.path.expanduser("~/Library/Logs/Micro Snitch.log")) as frb:
for line in frb:
index = line.find(": ")
if index < 0:
continue
line = line[index + 2:]
if line.startswith("Micro Snitch") or line == "Activity log enabled":
break
if "found" in line: # Ex: "Audio Device found: ...", "Video Device found (active): ..."
if audio_state is None and line.startswith("Audio Device"):
audio_state = "(active)" in line
if video_state is None and line.startswith("Video Device"):
video_state = "(active)" in line
elif "became" in line:
if audio_state is None and line.startswith("Audio Device"):
audio_state = "inactive" not in line
if video_state is None and line.startswith("Video Device"):
video_state = "inactive" not in line
if audio_state is not None and video_state is not None:
break
if audio_state is None:
audio_state = False
if video_state is None:
video_state = False
return audio_state, video_state
if __name__ == "__main__":
if COMMAND_FILE_LOCATION is not None:
with open(COMMAND_FILE_LOCATION, "r") as f:
command_file_content = f.readline().strip()
if COMMAND_FILE_MODE == "read":
light_color_command = command_file_content
else:
audio, video = is_mic_video_active()
light_color_command: str
output_str: str
if video:
light_color_command = "RED"
else:
light_color_command = "OFF"
if light_color_command == "OFF":
output_str = "◻️"
elif light_color_command == "RED":
output_str = "🟥"
elif light_color_command == "GREEN":
output_str = "🟩"
elif light_color_command == "ORANGE":
output_str = "🟧"
elif light_color_command == "YELLOW":
output_str = "🟨"
elif light_color_command == "BLUE":
output_str = "🟦"
elif light_color_command == "PURPLE":
output_str = "🟪"
else:
output_str = "❓"
connection_error: Optional[str] = None
if COMMAND_FILE_MODE == "write" and COMMAND_FILE_LOCATION is not None:
# Only write on change
if command_file_content != light_color_command:
try:
with open(COMMAND_FILE_LOCATION, "w") as f:
f.write(f"{light_color_command}\n")
except Exception as e:
output_str += "⚠️"
connection_error = f"Unable to write file: {e}"
pass
if LIGHT_ADDRESS is not None:
try:
requests.get(f"http://{LIGHT_ADDRESS}/", {"pin": light_color_command}, timeout=3)
except requests.exceptions.ConnectionError:
output_str += "⚠️"
connection_error = "Unable to connect to light"
pass
utf_to_stdout(output_str)
utf_to_stdout("---")
if connection_error:
utf_to_stdout(connection_error)
utf_to_stdout("Refresh | refresh=true")
// From https://hackaday.io/project/176566-tiny-wifi-busy-light
#include <Adafruit_NeoPixel.h>
#include <ESP8266WiFi.h>
char ssid[] = ""; // your network SSID (name)
char pass[] = ""; // your network password
IPAddress ip(192, 168, 2, 239); // If you want to have a fixed ip address, set it up here
IPAddress gateway(192, 168, 2, 1);
IPAddress subnet(255, 255, 255, 0);
String CColor = "";
unsigned long ulReqcount;
unsigned long ulReconncount;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(2, 15);
WiFiServer server(80);
void setup()
{
// setup globals
ulReqcount = 0;
ulReconncount = 0;
strip.begin();
strip.show();
//WiFi.config(ip, gateway, subnet); // comment this out if you want to get an ip address from dhcp-server
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
int itry = 0;
while (WiFi.status() != WL_CONNECTED) {
strip.begin();
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 255, 255);
}
strip.setBrightness(128),
strip.show();
delay(500);
strip.begin();
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 0, 0, 0);
}
strip.setBrightness(128),
strip.show();
delay(500);
itry++;
if(itry>=20) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 255, 255);
}
while(true) {
for(int i = 0; i<255; i++) {
strip.begin();
strip.setBrightness(i),
strip.show();
delay(100);
}
for(int i = 0; i<255; i++) {
strip.begin();
strip.setBrightness(255-i),
strip.show();
delay(100);
}
delay(1000);
}
}
}
server.begin();
}
void changeColor(uint32_t c) {
for(uint16_t i=0; i<strip.numPixels(); i++) {
strip.setPixelColor(i, c);
}
strip.show();
}
void colorWipe(uint32_t c, uint8_t wait) {
for(uint16_t i=0; i<strip.numPixels(); i++) {
strip.setPixelColor(i, c);
delay(wait);
strip.show();
delay(wait);
}
}
void rainbow(uint8_t wait) {
uint16_t i, j;
for(j=0; j<256; j++) {
for(i=0; i<strip.numPixels(); i++) {
strip.setPixelColor(i, Wheel((i+j) & 255));
}
strip.show();
delay(wait);
}
}
void rainbowCycle(uint8_t wait) {
uint16_t i, j;
for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
for(i=0; i< strip.numPixels(); i++) {
strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));
}
strip.show();
delay(wait);
}
}
//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait) {
for (int j=0; j<10; j++) { //do 10 cycles of chasing
for (int q=0; q < 3; q++) {
for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
strip.setPixelColor(i+q, c); //turn every third pixel on
}
strip.show();
delay(wait);
for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
strip.setPixelColor(i+q, 0); //turn every third pixel off
}
}
}
}
//Theatre-style crawling lights with rainbow effect
void theaterChaseRainbow(uint8_t wait) {
for (int j=0; j < 256; j++) { // cycle all 256 colors in the wheel
for (int q=0; q < 3; q++) {
for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
strip.setPixelColor(i+q, Wheel( (i+j) % 255)); //turn every third pixel on
}
strip.show();
delay(wait);
for (uint16_t i=0; i < strip.numPixels(); i=i+3) {
strip.setPixelColor(i+q, 0); //turn every third pixel off
}
}
}
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
void loop()
{
WiFiClient client = server.available();
if (!client) {
return;
}
unsigned long ultimeout = millis() + 250;
while (!client.available() && (millis() < ultimeout)) {
delay(1);
}
if (millis() > ultimeout) {
return;
}
// Read the first line of the request
String sRequest = client.readStringUntil('\r');
client.flush();
// stop client, if request is empty
if (sRequest == "") {
client.stop();
return;
}
// get path; end of path is either space or ?
// Syntax is e.g. GET /?pin=MOTOR1STOP HTTP/1.1
String sPath = "", sParam = "", sCmd = "";
String sGetstart = "GET ";
int iStart, iEndSpace, iEndQuest;
iStart = sRequest.indexOf(sGetstart);
if (iStart >= 0) {
iStart += +sGetstart.length();
iEndSpace = sRequest.indexOf(" ", iStart);
iEndQuest = sRequest.indexOf("?", iStart);
// are there parameters?
if (iEndSpace > 0) {
if (iEndQuest > 0) {
// there are parameters
sPath = sRequest.substring(iStart, iEndQuest);
sParam = sRequest.substring(iEndQuest, iEndSpace);
}
else {
// NO parameters
sPath = sRequest.substring(iStart, iEndSpace);
}
}
}
///////////////////////////////////////////////////////////////////////////////
// output parameters to serial, you may connect e.g. an Arduino and react on it
///////////////////////////////////////////////////////////////////////////////
if (sParam.length() > 0) {
int iEqu = sParam.indexOf("=");
if (iEqu >= 0) {
sCmd = sParam.substring(iEqu + 1, sParam.length());
}
}
///////////////////////////
// format the html response
///////////////////////////
String sResponse, sHeader;
////////////////////////////
// 404 for non-matching path
////////////////////////////
if (sPath != "/") {
sResponse = "<html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>";
sHeader = "HTTP/1.1 404 Not found\r\n";
sHeader += "Content-Length: ";
sHeader += sResponse.length();
sHeader += "\r\n";
sHeader += "Content-Type: text/html\r\n";
sHeader += "Connection: close\r\n";
sHeader += "\r\n";
}
///////////////////////
// format the html page
///////////////////////
else {
ulReqcount++;
sResponse = "<html><head><title>BusyLight</title></head><body>";
sResponse += "<font color=\"#000000\"><body bgcolor=\"#000000\">";
sResponse += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">";
sResponse += "<h1>Office BusyLight</h1>";
//////////////////////
// react on parameters
//////////////////////
if (sCmd.length() > 0) {
// write received command to html page
sResponse += "Last Color Set:" + sCmd + "<BR>";
strip.begin();
// switch GPIO
if (sCmd.indexOf("RED") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 0, 0);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("GREEN") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 0, 255, 0);
}
strip.setBrightness(255),
strip.show();
}
else if (sCmd.indexOf("BLUE") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 0, 0, 255);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("YELLOW") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 255, 0);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("WHITE") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 255, 255);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("PURPLE") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 0, 255);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("ORANGE") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 255, 110, 0);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("CWR") >= 0) {
colorWipe(strip.Color(255, 0, 0), 20); // Red
}
else if (sCmd.indexOf("CWG") >= 0) {
colorWipe(strip.Color(0, 255, 0), 20); // Green
}
else if (sCmd.indexOf("CWB") >= 0) {
colorWipe(strip.Color(0, 0, 255), 20); // Blue
}
else if (sCmd.indexOf("TCRB") >= 0) {
theaterChaseRainbow(20);
}
else if (sCmd.indexOf("TCR") >= 0) {
theaterChase(strip.Color(255, 0, 0), 20); // Red
}
else if (sCmd.indexOf("TCG") >= 0) {
theaterChase(strip.Color(0, 255, 0), 20); // Green
}
else if (sCmd.indexOf("TCB") >= 0) {
theaterChase(strip.Color(0, 0, 255), 20); // Blue
}
else if (sCmd.indexOf("RBC") >= 0) {
rainbowCycle(20);
}
else if (sCmd.indexOf("RBOW") >= 0) {
rainbow(20);
}
else if (sCmd.indexOf("CYAN") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 0, 255, 255);
}
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("RAINBOW") >= 0) {
strip.setPixelColor(0, 255, 0, 0);
strip.setPixelColor(1, 255, 0, 128);
strip.setPixelColor(2, 255, 0, 255);
strip.setPixelColor(3, 0, 0, 255);
strip.setPixelColor(4, 0, 128, 255);
strip.setPixelColor(5, 0, 255, 0);
strip.setPixelColor(6, 0, 255, 128);
strip.setPixelColor(7, 128, 255, 0);
strip.setPixelColor(8, 255, 255, 0);
strip.setPixelColor(9, 0, 255, 255);
strip.setPixelColor(10, 0, 255, 255);
strip.setPixelColor(10, 0, 255, 255);
strip.setPixelColor(11, 0, 255, 255);
strip.setBrightness(128),
strip.show();
}
else if (sCmd.indexOf("OFF") >= 0) {
for(int i = 0; i<12; i++) {
strip.setPixelColor(i, 0, 0, 0);
}
strip.setBrightness(128),
strip.show();
}
}
sResponse += "</body></html>";
sHeader = "HTTP/1.1 200 OK\r\n";
sHeader += "Content-Length: ";
sHeader += sResponse.length();
sHeader += "\r\n";
sHeader += "Content-Type: text/html\r\n";
sHeader += "Connection: close\r\n";
sHeader += "\r\n";
}
// Send the response to the client
client.print(sHeader);
client.print(sResponse);
// and stop the client
client.stop();
}
@alos-source
Copy link

Just stumpled over this gist, via your comment on https://hackaday.io/project/180751-5-wifi-busy-light-2021-had-prize-entry
If been looking for a the same use-case but for Win10 environments. Do you know if there is anything similar?

@robeden
Copy link
Author

robeden commented Jan 7, 2024

Hi @alos-source. The python script and approach should work fine with the exception that I don't know of something similar to MicroSnitch which would log when the camera or microphone are in use. I did a quick search but didn't immediately find anything. If you can find something that logs when they're active or inactive, then it could easily be modified to work, I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment