# Project export: Canary

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: Satellite-enabled wireless sensor network for early wildfire detection.
- Devpost: https://devpost.com/software/canary-lrg3ep
- GitHub: https://github.com/tsai-henry/canary
- Video: https://www.youtube.com/embed/OwAMRadd5Y8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Skylo: Seamlessly Switch from Cellular to Satellite)
- Team: 5 GitHub contributor(s) — Pranay Jha (6 commits), Niklichang (5 commits), Henry Tsai (5 commits), StaceyL (4 commits), tsai-henry (1 commits)

## Devpost submission (written by the team)

### Inspiration

🔥 While on the way to CalHacks, we drove past a fire in Oakland Hills that had started just a few hours prior, meters away from I-580. Over the weekend, the fire quickly spread and ended up burning an area of 15 acres, damaging 2 homes and prompting 500 households to evacuate. This served as a harsh reminder that wildfires can and will start anywhere as long as few environmental conditions are met, and can have devastating effects on lives, property, and the environment. The following statistics are from the year 2020[1]. People: Wildfires killed over 30 people in our home state of California. The pollution is set to shave off a year of life expectancy of CA residents in our most polluted counties if the trend continues. Property: We sustained $19b in economic losses due to property damage. Environment: Wildfires have made a significant impact on climate change. It was estimated that the smoke from CA wildfires made up 30% of the state’s greenhouse gas emissions. UChicago also found that “a single year of wildfire emissions is close to double emissions reductions achieved over 16 years.” Right now (as of 10/20, 9:00AM): According to Cal Fire, there are 7 active wildfires that have scorched a total of approx. 120,000 acres. [1] - news.chicago.edu Our Solution: Canary 🐦🚨 Canary is an early wildfire detection system powered by an extensible, low-power, low-cost, low-maintenance sensor network solution. Each sensor in the network is placed in strategic locations in remote forest areas and records environmental data such as temperature and air quality, both of which can be used to detect fires. This data is forwarded through a WiFi link to a centrally-located satellite gateway computer. The gateway computer leverages a Monogoto Satellite NTN (graciously provided by Skylo) and receives all of the incoming sensor data from its local network, which is then relayed to a geostationary satellite. Back on Earth, we have a ground station dashboard that would be used by forest rangers and fire departments that receives the real-time sensor feed. Based on the locations and density of the sensors, we can effectively detect and localize a fire before it gets out of control. What Sets Canary Apart 💡 Current satellite-based solutions include Google’s FireSat and NASA’s GOES satellite network. These systems rely on high-quality imagery to localize the fires, quite literally a ‘top-down’ approach. Google claims it can detect a fire the size of a classroom and notify emergency services in 20 minutes on average, while GOES reports a latency of 3 hours or more. We believe these existing solutions are not effective enough to prevent the disasters that constantly disrupt the lives of California residents as the fires get too big or the latency is too high before we are able to do anything about it. To address these concerns, we propose our ‘bottom-up’ approach, where we can deploy sensor networks on a single forest or area level and then extend them with more sensors and gateway computers as needed. Technology Details 🖥️ Each node in the network is equipped with an Arduino 101 that reads from a Grove temperature sensor. This is wired to an ESP8266 that has a WiFi module to forward the sensor data to the central gateway computer wirelessly. The gateway computer, using the Monogoto board, relays all of the sensor data to the geostationary satellite. On the ground, we have a UDP server running in Google Cloud that receives packets from the satellite and is hooked up to a Streamlit dashboard for data visualization. Challenges and Lessons 🗻 There were two main challenges to this project. Hardware limitations: Our team as a whole is not very experienced with hardware, and setting everything up and getting the different components to talk to each other was difficult. We went through 3 Raspberry Pis, a couple Arduinos, different types of sensors, and even had to fashion our own voltage divider before arriving at the final product. Although it was disheartening at times to deal with these constant failures, knowing that we persevered and stepped out of our comfort zones is fulfilling. Satellite communications: The communication proved to be tricky due to inconsistent timing between sending and receiving the packages. We went through various socket ids and ports to see if there were any patterns to the delays. Through our thorough documentation of steps taken, we were eventually able to recognize a pattern in when the packages were being sent and modify our code accordingly. What’s Next for Canary 🛰️ As we get access to better sensors and gain more experience working with hardware components (especially PCB design), the reliability of our systems will improve. We ran into a fair amount of obstacles with the Monogoto board in particular, but as it was announced as a development kit only a week ago, we have full faith that it will only get better in the future. Our vision is to see Canary used by park services and fire departments in the most remote areas of our beautiful forest landscapes in which our satellite-powered sensor network can overcome the limitations of cellular communication and existing fire detection solutions.

## README (from the GitHub repository)

# canary
CalHacks 11.0


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 11 KB.
- Python (language) — detected in the code
- Streamlit (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (16 of 16)

```
.gitignore
ino/child_sensor/child_sensor.ino
ino/esp/esp.ino
ino/hub_sensor/hub_sensor.ino
README.md
rx/reqs.txt
rx/rx_esp.py
rx/rx_hub.py
sensor_data/child1.txt
sensor_data/hub.txt
skylo/log_of_working_serial_commands.txt
skylo/python_at.py
skylo/udp_client.py
skylo/udp_pandas_server.py
skylo/udp_server.py
streamlit_stacey.py
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- again
- Merge branch 'main' of https://github.com/tsai-henry/canary
- added diffs to rx
- Merge branch 'main' of https://github.com/tsai-henry/canary
- added tuple into glue
- updated pandas server with tuple checking for temp source
- updated server
- rx rx rx
- gitignore change
- hub glue
- hide glue
- glue
- Merge branch 'main' of github.com:tsai-henry/canary
- big checklist time wooo
- update hub ino
- sensors and rx_esp
- class version of at cmds
- first implementation of AT commands in python, no OOP classes
- start live share
- improved client string formatting and added loop

## Key source files (fetched from GitHub, selected and truncated for size)

### streamlit_stacey.py

```python
import streamlit as st


import numpy as np
import pandas as pd
import time
import matplotlib.pyplot as plt


st.markdown('<h1 class="center">WildFire Data Visualization</h1>', unsafe_allow_html=True)
st.write('<h6 class="center">Data is continuously updated based on sensor inputs.</h6>', unsafe_allow_html=True)


# Generate a date range starting from now with 100 points at 5-minute intervals
date_range = pd.date_range(start='2024-10-19 00:00', periods=50, freq='5T')


# Generate random temperatures between 70 and 90 degrees
temperatures = np.random.uniform(50, 100, size=50)
temperatures_s2 = np.random.uniform(50, 60, size=50)


# Create a DataFrame
#temperature_data_1 = pd.DataFrame({
#    'Time': date_range,
#    'Temperature (°F)': temperatures
#})
#temperature_data_2 = pd.DataFrame({ #second sensor's information
#    'Time': date_range,
#   'Temperature (°F)': temperatures_s2
#})
container = st.beta_container()
all = st.checkbox("Select all")
 
if all:
    selected_options = container.multiselect("Select one or more regions:",
         ['Region 1', 'Region 2'],['Region 1', 'Region 2'])
else:
    selected_options =  container.multiselect("Select one or more options:",
        ['Region 1', 'Region 2'])
   


col1, col2 = st.columns([1, 3]) #set columns for the sensors and data


with col1:
    st.markdown('<h4>Region Sensors</h4>', unsafe_allow_html=True)
    st.write("Region 1")
    st.write("Region 2")
with col2:
    #df = pd.DataFrame(temperature_data_1)
    #df.set_index("Time")
    #df['Temperature (°F)'].plot()




    plt.title("Temperature Over Time")
    plt.xlabel("Time (minutes)")
    plt.ylabel("Temperature (°F)")


    plt.plot(date_range, temperatures, label="sensor1")
    plt.plot(date_range, temperatures_s2, label="sensor2")
    plt.xticks(rotation = 35)
    #plt.legend()
    st.pyplot(plt)
print(date_range)
#st.dataframe(df)


# Centering title
st.markdown(
    """
    <style>
    .center {
        text-align: center;
    }
    </style>
    """,
    unsafe_allow_html=True
)





```

### skylo/udp_server.py

```python
import socket

# 1. Create a UDP socket
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 2. Bind the socket to all network interfaces
address = "0.0.0.0"
port = 51820
server_address = (address, port)
udp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
udp_server.bind(server_address)

print(f"UDP server is listening on {server_address}")

# 3. Receive and respond to data
while True:
    data, client_address = udp_server.recvfrom(1024)
    print(f"Received {data.decode()} from {client_address}")

    # Send a response
    message = "Message received".encode()
    udp_server.sendto(message, client_address)

```

### skylo/udp_client.py

```python
import socket
from time import sleep
from datetime import datetime

# Create a UDP client socket
udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Server address (replace with your VM's external IP)
address = "34.19.125.150"
port = 51820
server_address = (address, port)
message = "Hello! This default message is set before the for loop."
iterations = 10
delay = 2

# Send data to the server
for i in range(iterations):
    message = f"Packet {i} sent at {datetime.now()}"
    udp_client.sendto(message.encode(), server_address)
    
    # Receive response from the server
    response, _ = udp_client.recvfrom(1024)
    print(f"Received from server: {response.decode()}")
    
    sleep(delay)

```

### rx/rx_esp.py

```python
import socket

esp8266_ip = "192.168.4.1"  # default ip
esp8266_port = 8080 

# tcp socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.settimeout(10)  # 10s timeout

diff = -200

try:
    print("Attempting to connect to ESP8266...")
    client_socket.connect((esp8266_ip, esp8266_port))
    print("Connected to ESP8266!")

    while True:
        try:
            data = client_socket.recv(1024).decode().strip()
            data = int(data) + diff
            print("Received:", data)
            with open("../sensor_data/child1.txt", 'w') as f:
                f.write(f"{data},1")
        except KeyboardInterrupt:
            break
        except:
            print("Waiting for data stream...")

finally:
    client_socket.close()
    print("\nConnection closed.")


```

### rx/rx_hub.py

```python
import serial
import time

serial_port = '/dev/cu.usbmodem1101'
baud_rate = 9600
output_file = '../sensor_data/hub.txt'

diff = -100

def capture_serial_data():
    try:
        with serial.Serial(serial_port, baud_rate, timeout=1) as ser:
            while True:
                if ser.in_waiting > 0:
                    data = ser.readline().decode('utf-8').strip()  # get data
                    data = int(data) + diff
                    print(f"Received: {data}") 
                    
                    with open(output_file, 'w') as file:
                        file.write(f"{data},0") #overwrite first line
                time.sleep(1)

    except serial.SerialException as e:
        print(f"Error: {e}")
    except KeyboardInterrupt:
        print("\nExiting...")
        
if __name__ == "__main__":
    capture_serial_data()

```

### skylo/udp_pandas_server.py

```python
import socket
import pandas as pd
from datetime import datetime

CHILD_IP = "1.1.1.1"
HUB_IP = "2.2.2.2"

child_temp_diff = -140
hub_temp_diff = -150

def calibrate_child_temp(celsius_temp):
    fahrenheit = 1.8 * celsius_temp + 32
    return fahrenheit + child_temp_diff

def calibrate_hub_temp(celsius_temp):
    fahrenheit = 1.8 * celsius_temp + 32
    return fahrenheit + hub_temp_diff

# Create UDP server
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
address = "0.0.0.0"
port = 51820
server_address = (address, port)
udp_server.bind(server_address)
print(f"UDP server is listening on {server_address}")

# Create dataframe to store data
columns = ["timestamp", "source", "data"]
df = pd.DataFrame(columns=columns)

# Function to save a new packet to the DataFrame
def save_packet_to_dataframe(timestamp, source, data):
    global df  # Reference the global DataFrame
    source = "child" if (source == 0) else "hub"
    new_entry = pd.DataFrame([[timestamp, source, data]], columns=columns)
    df = pd.concat([df, new_entry], ignore_index=True)
    print(f"Saved packet from {source} at {timestamp}")

try:
    while True:
        # Receive data from the client
        source = "unknown"
        data, client_address = udp_server.recvfrom(1024)  # Buffer size 1024 bytes
        decoded_data = data.decode()
        temp = decoded_data[0]
        source = decoded_data[1]
        
        # Capture the current timestamp
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        # 0 is child sensor tag, 1 is hub sensor tag
        if source == 0
            source = "child"
        else:
            source = "hub"

        # Save the packet to the DataFrame
        save_packet_to_dataframe(timestamp, source, decoded_data)

        # Optional: Send a confirmation response to the client
        udp_server.sendto("Packet received".encode(), client_address)

except KeyboardInterrupt:
    print("\nServer stopped by user.")

finally:
    # Optional: Save DataFrame to a CSV file on exit
    df.to_csv("udp_packets.csv", index=False)
    print("Data saved to udp_packets.csv")

```

### skylo/python_at.py

```python
import time
import serial


COM_PORT = r'\\.\COM8'

class ComputerSerial:
    def __init__(self, address: str, 
                 baudrate: int = 115200,
                 timeout: int = 0,
                 wait_time: int = 1,
                 latitude = "37.783860",
                 longitude = "-122.402311",
                 string_encoding = 'utf-8',
                 ):
        """
        Initialize the serial connection and configure basic settings.
        
        Parameters:
        address (str): Serial port address (e.g., "/dev/ttyACM0").
        baudrate (int): Communication speed in baud (default: 460800).
        timeout (int): Timeout value for the serial connection in seconds (default: 0).
        wait_time (int): Time to wait after sending a command (default: 1 second).
        latitude
        longitude
        string_encoding
        """
        self.address = address
        self.baudrate = baudrate
        self.timeout = timeout
        self.wait_time = wait_time
        self.latitude = latitude
        self.longitude = longitude
        self.string_encoding = string_encoding
        
        # Initialize the serial connection
        self.connection = serial.Serial(address, baudrate, timeout=timeout)
        

        # Define AT commands
        self.commands = {
            "fun": f'AT+cfun=1\r',
            "pos": f'AT%NTNCFG="POS","STAT","{self.latitude}","{self.longitude}","160"\r',
            "allocate": f'AT%SOCKETCMD="ALLOCATE",0,"UDP","OPEN","34.19.125.150",51820\r',
        }

        # Initialize the Skylo board
        self.init_skylo()

        self.commands["setopt"] = f'AT%SOCKETCMD="SETOPT",{self.socket},36000\r'
        self.commands["activate"] = f'AT%SOCKETCMD="ACTIVATE",{self.socket}\r'
        self.commands["send"] = f'AT%SOCKETDATA="SEND",{self.socket},13,'  # Trailing comma for message
        self.commands["info"] = f'AT%SOCKETCMD="INFO",{self.socket}'
        self.commands["close"] = f'AT%SOCKETCMD="DELETE",{self.socket}'
        
        self.send_command(self.commands["setopt"])
        self.send_command(self.commands["activate"])

    def send_command(self, command: str):
        """
        Send a command over the serial connection after encoding to binary.
        
        Parameters:
        command (str): The command to send.
        """
        self.connection.write(command.encode(self.string_encoding))
        time.sleep(self.wait_time)

    def init_skylo(self):
        """
        Run the initialization commands for setting up Skylo to send UDP packets.
        """
        self.send_command(self.commands["fun"])
        self.send_command(self.commands["pos"])
        self.send_command(self.commands["allocate"])


    def send_udp(self, message: str):
        """
        Send a UDP packet with the specified message.
        
        Parameters:
        message (str): The message to send via UDP.
        """
        # Prepare the message with the command
        command_with_message = self.commands["send"] + message.encode(self.string_encoding) + b'\r'
        self.send_command(command_with_message)

    def close(self):
        """
        Close the serial connection.
        """
        self.send_command(self.commands["close"])

# Example usage:
if __name__ == "__main__":
    # Initialize the serial connection
    raspi = ComputerSerial(address=COM_PORT)

    try:
        # Initialize Skylo settings
        raspi.init_skylo()

        # Send a UDP message
        raspi.send_udp("Hello, World!")

    finally:
        # Close the connection when done
        raspi.close()


# AT commands from Stacey:

#OTHER CMDS
# AT+CFUN=0
# AT%IGNSSEV="FIX",1
# AT%NOTIFYEV="SIB31",1
#AT+CEREG=2
#AT%IGNSSACT=1





# AT+cfun=1
# AT%NTNCFG="POS","STAT","37.783860","-122.402311","160"
# ----- start extra commands ----------
# AT+CFUN=0
# AT%IGNSSEV="FIX",1
# AT%NOTIFYEV="SIB31",1
#AT+CEREG=2
#AT%IGNSSACT=1
# ------- end extra commands ---------

# AT%SOCKETCMD="ALLOCATE",0,"UDP","OPEN","34.19.125.150",51820
# AT%SOCKETCMD="SETOPT",2,36000
# AT%SOCKETCMD="ACTIVATE",2
# AT%SOCKETDATA="SEND",2,13,"48656C6C6F2C20776F726C6421"

def s(command):
    pc.write((command + '\r').encode())
    sleep(2)
    return pc.read



```