Software
Assets
- Legacy Repo: https://github.com/usdevs/laundry-pi
- Legacy Logs Dump: https://drive.google.com/drive/folders/1K-hUIznR1B5sm4qYlegwSkagch1LGY3G?usp=sharing
Logs Dump contains
.csv
and.log
that is generated by the previous implementation
Legacy Setup
git clone https://github.com/usdevs/laundry-pi.git
Install dependencies:
pip install firebase-admin Adafruit-Blinka Adafruit-ADS1x15
References
Setup device specific configuration in config.py
config.py
stores parameters needed for database (firestore) access and device specific configuration.
Generate a firestore cert, and save it somewhere on the RPI
- This involves creating a firebase service account, and generating a cert for it. (reference)
Copy
config.py.example
asconfig.py
and fill in it in appropriately.FIRESTORE_CERT
refers to the path to the firestore cert file we generated above.
Scheduling
Start
main.py
when the RPI boots upcrontab -e @reboot cd <path-to-laundro-src> && python3 runner.py main.py main`
Check upstream repo for new updates at a interval of 1 minute
crontab -e * * * * * cd path/to/laundro && git pull && git log --pretty=oneline -1 | python3 flagger.py
How it Works
main.py
This is where the main control loop resides and the entrypoint of the program.
info
Also provides a control loop (local_main
) that updates pin values (to a .csv
) without connection to firebase. This is called when an error is thrown when triggering the actual control loop.
def main():
# Initiates a logger
init_logger(config.LOGDIR)
log = logging.getLogger()
log.info('main script started.')
# Set up Firestore connection
firestore = FirestoreManager(config.FIRESTORE_CERT, config.PI_ID)
# Get the list of pins
# Only grabs the pins that are allocated to config.PI_ID (more on this below)
pins = get_pins(config.PI_ID)
flag = flagger.Flag(flagger.flag)
# Creates firestore documents for the pins and rpi, if it doesn't exist already
firestore.init_pins(map(lambda p: p.id, pins))
firestore.init_pi()
# Grab last recorded pin readings from firestore
prev_on = {} # { <pin_id: int> : is_on: bool }
washer_ids = firestore.get_washing_machine_pin_ids()
for p in pins:
on = p.is_on()
prev_on[p.id] = on
# For each pin, get the previous time when it was updated from firebase
current = firestore.get_pin_data(p.id)
now = sg_time_now()
timediff = now - current['timeChanged']
'''
If the the status of the pin is unchanged for a period under 30 min (for washers) / 45 min (for dryers) we not modify the pin record
Intuition:
- To update timeChanged of the pin status upon script start
- However since we are not certain of what is the exact timestamp this change happen, timeChangedCertain = False
Possible Concern:
- Can we actually trust the clocks of the RPI?
'''
if on == current['on'] and p.id in washer_ids and timediff <= dt.timedelta(minutes=30) or \
timediff <= dt.timedelta(minutes=45):
continue
else:
firestore.update_pin(p.id, on, sg_time_now(), timeChangedCertain = False)
'''
Assumes that each run of the loop takes around 25 seconds
Observation:
Runtime of one iteration ~= 2secs * 9pins + 1 secs sleep (around 21-22 secs)
2secs * 9pins : More on this in `pin.py`
'''
seconds = 0
while True:
# Every 6 minutes (24 runs), update the `lastSeen` time for the RPi to the current time.
if seconds == 600:
firestore.update_pi_last_seen()
seconds = 0
# Check if any pins have changed
for p in pins:
on = p.is_on()
if on != prev_on[p.id]:
firestore.update_pin(p.id, on, sg_time_now())
prev_on[p.id] = on
seconds = 0
# Check for updates from Github
if flag.flagged():
flag.unflag()
log.info('changes from github were detected. restarting main script.')
break
time.sleep(1) # I don't know why we are sleeping for 1 second, but it is there I guess
seconds += 25
log.debug("{} seconds".format(seconds))
get_pins(pi_id: int): List[Pin]
: Returns an array of Pin
s associated with each RPI
If pi_id == 1
, returns
Class | Pin id | ADC ID | ADC Pin id | record_values | threshold |
---|---|---|---|---|---|
Pin | 1 | ads1 | ADS.P0 | True | 32000 (Default) |
Pin | 2 | ads1 | ADS.P1 | True | 32000 (Default) |
Pin | 3 | ads1 | ADS.P2 | True | 32000 (Default) |
Pin | 4 | ads1 | ADS.P3 | True | 32000 (Default) |
Pin | 5 | ads2 | ADS.P0 | True | 20000 |
Pin | 6 | ads2 | ADS.P1 | True | 32000 (Default) |
Pin | 7 | ads2 | ADS.P2 | True | 32000 (Default) |
Pin | 8 | ads2 | ADS.P3 | True | 32000 (Default) |
Pin | 9 | ads3 | ADS.P0 | True | 25000 |
If pi_id == 2
, returns
Class | Pin id | ADC ID | ADC Pin id | record_values | threshold |
---|---|---|---|---|---|
Pin | 10 | ads1 | ADS.P0 | True | 15000 |
Pin | 11 | ads1 | ADS.P1 | True | 32000 (Default) |
Pin | 12 | ads1 | ADS.P2 | True | 32000 (Default) |
Pin | 13 | ads1 | ADS.P3 | True | 21000 |
Pin | 14 | ads2 | ADS.P0 | True | 32000 (Default) |
Pin | 15 | ads2 | ADS.P1 | True | 32000 (Default) |
Pin | 16 | ads2 | ADS.P2 | True | 32000 (Default) |
Pin | 17 | ads2 | ADS.P3 | True | 32000 (Default) |
Pin | 18 | ads3 | ADS.P0 | True | 32000 (Default) |
Otherwise, raises an error
info
As their name implies, ADS.P0
, ADS.P1
, ADS.P2
and ADS.P3
corresponds to the pin values 0, 1, 2, and 3 respectively, and are int
values 0
, 1
, 2
, and 3
.
note
ads1
, ads2
and ads3
are ADS ids hardcoded as:
ads1 = ADS.ADS1115(i2c, address=0x48)
ads2 = ADS.ADS1115(i2c, address=0x49)
ads3 = ADS.ADS1115(i2c, address=0x4a)
runner.py
(TODO)
Importantly,
runner.py
takes amodule path
andfunction
and runsfunction
in a process. Upon the process terminating,module path
is reloaded and the newfunction
is run in a new process. This is used to run main.py
flagger.py
(TODO)
Previous repo says that it is a small ecosystem to update the code on the pi every time a new comit is made.
*.log
main.py
writes logs with the logging
module to the directory ~/laundro_logs/<pi ID>/
.
A log file can be read with live updates using:
tail -f <name of log file>
Log File | Description |
---|---|
all.log | Contains all log messages |
info.log | Contains only INFO level and above messages |
Practically speaking, log messages for individual sensor readings will be in all.log
and not info.log
sample
*.log
files can be find in the logs dump here
firestore_manager.py
(TODO)
pin.py
Contains the class definition for Pin
.
Each Pin
object represents a input pin of a ADC/ADS, and has the following properties and methods:
Properties | Info |
---|---|
id (int) | Unique ID to every pin (should be unique across RPis too) |
adc (adafruit_ads1x15.ads1115.ADS.ADS1115) | Represents one ADC module. (ADC Module that the pin connects to) |
adc_pin (adafruit_ads1x15.ads1115.ADS.P0/1/2/3) | Represents a pin on the ADC module. (ADC pin that pin connects to) |
threshold (int, optional) | Light threshold. The pin is on if the light value is below this threshold. Default = 32000 |
record_values (boolean) | Whether to record light value readings. Readings are recorded in a csv file named pin-<pin ID>.csv . |
(Instance) Methods | Info | Returns |
---|---|---|
is_on_single(self) | Checks if this pin is currently on, based on 1 reading at the moment. This may return off if the pin is blinking. | tuple(bool, int) |
is_on(self) | True if this pin is on or blinking, False otherwise. The pin will appear off if it is disconnected. | bool |
To test whether the pin is on (detecting light), an analog pin value is read via
light_value = adafruit_ads1x15.analog_in.AnalogIn(adc, adc_pin).value
and the light is considered on if light_value < self.threshold
.
is_on(self)
Core function that helps determine whether the washer/dryer is on or off
def is_on(self):
"""
Checks whether this pin is on, which includes blinking. Records light values to
a csv file, if self.record_values is True.
Returns:
True if this pin is on or blinking, False otherwise.
The pin will appear off if it is disconnected.
"""
log = logging.getLogger()
values = []
final_on = False
'''
Takes 20 readings over 2 seconds (every 0.1 seconds) and returns `True` if at least one detects the pin as on.
This is part where they account for blinking
'''
for i in range(20):
on, val = self.is_on_single()
values.append(val)
if not final_on and on:
final_on = True
time.sleep(0.1)
log.debug("{} is off. Values:{}".format(self, values))
# Logs light values into specified .csv file
if self.record_values:
with open('pin-' + str(self.id) + '.csv','a+') as f:
writer = csv.writer(f,quoting=csv.QUOTE_NONNUMERIC)
now = dt.datetime.now().isoformat()
rows = map(lambda v: (now,v), values)
writer.writerows(rows)
return final_on
sample
*.csv
files can be find in the logs dump here
config.py
Stores some config values:
Name | Description |
---|---|
FIRESTORE_CERT | The path to the JSON file containing the Firestore certificate. Used to create a new FirestoreManager |
PI_ID | Value is either 1 or 2 , is an identifier that determines which pins to update. Is called by get_pins(id) in main.py for a list of Pin objects. |
LOGDIR | Directory path to save log files in. Called in utils.init_logger(logdir="", logger_name=None) |