diff --git a/3dFiles/Images/README.md b/3dFiles/Images/README.md
new file mode 100644
index 0000000..138d507
--- /dev/null
+++ b/3dFiles/Images/README.md
@@ -0,0 +1 @@
+## Placeholder location to show final assembly instructions
diff --git a/3dFiles/README.md b/3dFiles/README.md
new file mode 100644
index 0000000..8ec22e0
--- /dev/null
+++ b/3dFiles/README.md
@@ -0,0 +1,85 @@
+# Simple Sat Hardware
+
+## Ground Station Portion of CPX Simple Sat
+
+Requirements to build and assembly a ground station portion of the SimpleSat kit are as follows:
+
+1. [Adafruit Circuit Playground Express](https://www.adafruit.com/product/3333)
+2. [Raspberry Pi](https://www.amazon.com/CanaKit-Raspberry-4GB-Basic-Kit/dp/B07TXKY4Z9)
+3. [Jumper Cables](https://www.adafruit.com/product/1953) or [Alligator clips](https://www.adafruit.com/product/1008)
+4. (Optional - current GS frame is built for this case) [Aluminum Pi 4 case](https://www.amazon.com/Geekworm-Raspberry-Computer-Aluminum-Compatible/dp/B07VD6LHS1/)
+
+## Satellite Portion of CPX Simple Sat
+
+Requirements to build and assemble a Satellite portion of the SimpleSat kit are as follows:
+
+1. [Adafruit Circuit Playground Express](https://www.adafruit.com/product/3333)
+2. [Adafruit CRICKIT for Circuit Playground Express](https://www.adafruit.com/product/3093)
+3. [Power Supply for CRICKIT](https://www.adafruit.com/product/276)
+4. STL files 3D printed to the quantity specified below.
+5. Nuts and bolts outlined below.
+
+## Simple Sat Enclosure
+
+### STL Files
+
+Print the .stl files in the following quantity:
+
+1. sat_upper_half - 1
+2. sat_lower_half - 1
+3. sat_solar_panel_arm - 2
+4. sat_solar_panel - 2
+5. sat_solar_panel_gear_18 - 2
+6. sat_radar_dish_arm - 2
+7. sat_radar_dish - 2
+
+### Hardware Requirements
+
+| Item | Quantity |
+| ---- | ----- |
+| M2.5x10 Bolt | 8 |
+| M2.5 Nut | 8 |
+| M2.5x20 Bolt | 4 |
+
+Alternatively, it may be cheaper and easier to simply purchase an assortment of M2.5 nuts and bolts.
+
+[Example Kit](https://www.amazon.com/HVAZI-Metric-304-tornillos-inoxidable/dp/B07F14J7X8/ref=sr_1_21?dchild=1&keywords=hvazi+m2.5+304+button+head&qid=1591969674&sr=8-21)
+
+
+In addition, the screws that came with the Micro Servos are also used to affix gears to them and to secure the motors in place.
+
+## Assembly Instructions
+
+### Servo Orientation
+
+The following diagram depicts the intended servo orientation when fully assembled.
+
+
+ -------
+ / \
+ / \
+ | Servo #4 |
+ | |
+ \ /
+ \ /
+ -------
+ ||
+ ||
+ |---------------|
+|---------------------\ | PWR | /---------------------|
+| \--| |--/ |
+| Servo #1 | | Servo #2 |
+| /--| |--\ |
+|---------------------/ | | \---------------------|
+ |---------------|
+ ||
+ ||
+ -------
+ / \
+ / \
+ | Servo #3 |
+ | |
+ \ /
+ \ /
+ -------
+
diff --git a/3dFiles/STL/gnd_dish_stand.stl b/3dFiles/STL/gnd_dish_stand.stl
new file mode 100644
index 0000000..5563109
Binary files /dev/null and b/3dFiles/STL/gnd_dish_stand.stl differ
diff --git a/3dFiles/STL/gnd_station_rpi_wrapper.stl b/3dFiles/STL/gnd_station_rpi_wrapper.stl
new file mode 100644
index 0000000..8fdb3ea
Binary files /dev/null and b/3dFiles/STL/gnd_station_rpi_wrapper.stl differ
diff --git a/3dFiles/STL/sat_lower_half.stl b/3dFiles/STL/sat_lower_half.stl
new file mode 100644
index 0000000..9d2de75
Binary files /dev/null and b/3dFiles/STL/sat_lower_half.stl differ
diff --git a/3dFiles/STL/sat_radar_dish.stl b/3dFiles/STL/sat_radar_dish.stl
new file mode 100644
index 0000000..a458246
Binary files /dev/null and b/3dFiles/STL/sat_radar_dish.stl differ
diff --git a/3dFiles/STL/sat_radar_dish_arm.stl b/3dFiles/STL/sat_radar_dish_arm.stl
new file mode 100644
index 0000000..1931a04
Binary files /dev/null and b/3dFiles/STL/sat_radar_dish_arm.stl differ
diff --git a/3dFiles/STL/sat_radar_dish_arm_v2.stl b/3dFiles/STL/sat_radar_dish_arm_v2.stl
new file mode 100644
index 0000000..516928c
Binary files /dev/null and b/3dFiles/STL/sat_radar_dish_arm_v2.stl differ
diff --git a/3dFiles/STL/sat_radar_dish_v2.stl b/3dFiles/STL/sat_radar_dish_v2.stl
new file mode 100644
index 0000000..91f0c77
Binary files /dev/null and b/3dFiles/STL/sat_radar_dish_v2.stl differ
diff --git a/3dFiles/STL/sat_solar_panel.stl b/3dFiles/STL/sat_solar_panel.stl
new file mode 100644
index 0000000..6290189
Binary files /dev/null and b/3dFiles/STL/sat_solar_panel.stl differ
diff --git a/3dFiles/STL/sat_solar_panel_arm.stl b/3dFiles/STL/sat_solar_panel_arm.stl
new file mode 100644
index 0000000..c68f4af
Binary files /dev/null and b/3dFiles/STL/sat_solar_panel_arm.stl differ
diff --git a/3dFiles/STL/sat_solar_panel_gear_18_tooth.stl b/3dFiles/STL/sat_solar_panel_gear_18_tooth.stl
new file mode 100644
index 0000000..db1a759
Binary files /dev/null and b/3dFiles/STL/sat_solar_panel_gear_18_tooth.stl differ
diff --git a/3dFiles/STL/sat_stand_connector.stl b/3dFiles/STL/sat_stand_connector.stl
new file mode 100644
index 0000000..6c11e43
Binary files /dev/null and b/3dFiles/STL/sat_stand_connector.stl differ
diff --git a/3dFiles/STL/sat_upper_half.stl b/3dFiles/STL/sat_upper_half.stl
new file mode 100644
index 0000000..d94652c
Binary files /dev/null and b/3dFiles/STL/sat_upper_half.stl differ
diff --git a/3dFiles/STL/smoker_backpack.stl b/3dFiles/STL/smoker_backpack.stl
new file mode 100644
index 0000000..59687ea
Binary files /dev/null and b/3dFiles/STL/smoker_backpack.stl differ
diff --git a/3dFiles/STL/smoker_clamp.stl b/3dFiles/STL/smoker_clamp.stl
new file mode 100644
index 0000000..f5084cb
Binary files /dev/null and b/3dFiles/STL/smoker_clamp.stl differ
diff --git a/3dFiles/playgroundSat.FCStd b/3dFiles/playgroundSat.FCStd
new file mode 100644
index 0000000..a78ea71
Binary files /dev/null and b/3dFiles/playgroundSat.FCStd differ
diff --git a/BONUS-cpx-sat-notes.pdf b/BONUS-cpx-sat-notes.pdf
new file mode 100644
index 0000000..378030d
Binary files /dev/null and b/BONUS-cpx-sat-notes.pdf differ
diff --git a/README.md b/README.md
index 80e99c3..f6d1dbd 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,36 @@
-# CPXSimpleSat
-Repo for DEFCON28 CPXSimpleSat
+# CPX Simple Sat
+
+Created as a part of the [Aerospace Village](https://aerospacevillage.org/) for DEFCON28 (Rest in Safe Mode), CPX Simple Sat attempts to teach the basics of satellite exploitation with a ground station in an easily approachable and jargon free manner.
+
+|  |
+| :---: |
+| How the setup looked like on our side |
+
+## Background
+
+Satellite hacking is one of those fields where all the best stories are kept behind closed doors. Like the rest of Aerospace, the field suffers from a large amount of jargon that can make it difficult for the casual passerby to understand and follow. Add to that the lack of thorough public documentation when events do happen, and it's easy to see why there are problems.
+
+In CPX Simple Sat we attempt to teach you some of the basic dangers and risks of satellite exploitation by creating an abstract version of a real world event. In 2008, hackers used a ground station to communicate to and attempt to gain access to the Terra satellite (launched in 1999). In the real world this is where the story ends, the hackers attempted to communicate with the satellite but didn't issue any commands, and the satellite itself was fine and is still working today. But while the real life event may not have been that exciting, the concept this event represents poses an interesting challenge.
+
+## The Game
+
+In CPX Simple Sat, you the player have come into possession of a SpaceDotCom ground station, giving you the perfect chance to attack SpaceDotCom's CPX Simple Sat. Why do you want to attack this random satellite you ask? Maybe you got tired of all of Otter MiSpace's random otter facts (Did you know there are 13 species of Otters?), maybe he hired you to do this, or maybe you figured what else would you do when you wake up with a ground station in your home. We just make the game, you can RP it however you like. No one is here to judge.
+
+Either way, you will be using a [Circuit Playground Express](https://www.adafruit.com/product/3333) (CPX) and [Raspberry Pi](https://www.raspberrypi.org/) that acts as the ground station you now control. Due to COVID-19, you will be interacting with your ground station through a twitch chat bot. To send a command to the chatbot, the message must start with "!", and all parameters are separated with a space. For example, to log into the system, the twitch user would type "!cmd login `username` `password`". Because there will be multiple people playing the game at once, we implemented a simple user management system by requiring users to type "!join" to get added to the user list and start playing the game. Once on the list, each user will get one minute to complete their turn before it moves on to the next user in the list. Your individual progress is saved and kept isolated from other users, so don't worry that you can't finish the game in one round.
+
+The satellite you will be interacting with is made out of a [Circuit Playground Express](https://www.adafruit.com/product/3333) and [CRICKIT](https://www.adafruit.com/product/3093). It talks to the ground station via the Inferred receiver and transmitter on each CPX board. Due to this, sometimes a message may not be decoded properly. If you think the state of the satellite is incorrect, then type "!reset" on your turn to resync it to your current state in the game.
+
+Using the game ICD, your goal in the game is to gain as much control of the satellite as you can by outwitting both its onboard systems and the remote ground station controlling it.
+
+### Game Documents
+
+- [Game ICD](./SimpleSat_ICD.pdf)
+- [Bonus Notes](./BONUS-cpx-sat-notes.pdf)
+
+## DIY
+
+If you want to create your own version of CPX Simple Sat, the STL files, build instructions, and source code can be found here:
+
+- [STL files and Assembly instructions](./3dFiles/README.md)
+- [Build Guide](./buildGuide.md)
+- [Source Code](./code/README.md)
diff --git a/SimpleSat_ICD.pdf b/SimpleSat_ICD.pdf
new file mode 100644
index 0000000..6949d58
Binary files /dev/null and b/SimpleSat_ICD.pdf differ
diff --git a/buildGuide.md b/buildGuide.md
new file mode 100644
index 0000000..c817614
--- /dev/null
+++ b/buildGuide.md
@@ -0,0 +1,124 @@
+# CPX SimpleSat Assembly Guide
+
+## Parts needed
+
+### 3D Printed pieces
+
+Essential parts:
+
+* 2 Solar Panel Arms
+* 2 Solar Panels
+* 2 Antenna Dishes
+* 2 Antenna Arms
+* 2 Servo Gears
+* 1 Satellite Upper body
+* 1 Satellite Lower body
+
+Nonessential parts, but needed if doing smoker modification:
+
+* 1 Smoker upper shell
+* 1 smoker lower shell
+
+### Commerical Parts
+
+Essential parts:
+
+* [Screw kit](https://www.amazon.com/DYWISHKEY-Pieces-Stainless-Socket-Washers/dp/B082XPZV1V)
+* [Micro Servo](https://www.amazon.com/Micro-Servos-Helicopter-Airplane-Controls/dp/B07MLR1498/)
+* [Adafruit CRICKIT](https://www.adafruit.com/product/3093)
+* 2x [Circuit Playground Express](https://www.adafruit.com/product/3333)
+* Raspberry Pi
+
+For Smoker modification:
+
+* [Diminus mini air pump](https://www.amazon.com/gp/product/B06Y2CXZ67/)
+* 1/4" silicone tubbing
+* 2 KangerTech T2 esig
+
+## Build instructions
+
+### Satellite
+
+The satellite part of CPX Simple Sat is the most complicated part of the build, so we'll tackle that first. Note the satellite will function with or without the smoke kit, so it's your choice if you want to add it.
+
+| Build Steps |
+| --- |
+|  |
+| Here are all the parts we'll need to build the satellite. Note for this build we painted the central part of the satellite gold to mimic the look of the multi-layer insulation on most satellite. |
+|  |
+| Here are the holes drilled to allow the smoker to produce smoke from underneath the CRICKIT board. This step isn't required, but it does make it look cool |
+|  |
+| For the first part of the build, we'll put together the antenna controller arms. Make sure all the micro servos spin smoothly before assembly|
+|  |
+| Find the side of the arm that has divots. this will be the top of the arm |
+|  |
+| To start, run the servo cable through the cavity within the arm |
+|  |
+| After sliding the micro servo into the gap, lock it in place by screwing it in using one of the small screws that came with the micro servo |
+|  |
+| Now snap on the the antenna dish. The hole at the bottom of the dish should be a snug fit for the white gear on the servo. If it feels loose, use another screw from the micro servo kit to make it a tighter fit |
+|  |
+| After making both control antenna arms, lets move onto the solar panels. First find the solar gears and push them onto the two remaining micro servos, just like we did with the antenna dish |
+|  |
+| After doing this twice, you've now finished assembling your servo components |
+|  |
+|  |
+| This step is optional but if you drilled holes for the smoker, now would be a good time to attach part of the tubing to the front panel. The more flush the tubing is to the hole, the more even the spread of smoke. I like to use hot glue to hold this end of the tubing in place while we assemble everything else |
+|  |
+|  |
+| Now would also be a good time to measure out how much tubing you will need to route through the satellite. Give your self a little extra to play safe, and then cut off the extra tubing |
+|  |
+| Now route the servo cable from the antenna arms through the back of the front panel |
+|  |
+| Using 10mm screws, attach the antenna arms to the front panel. Make sure everything is facing the right direction |
+|  |
+|  |
+| Using a nut, lock the arm in place on the other side. You can also use spacers between the arm and panel, as well as the nut and panel to stop the screw from digging into the pla if you tighten to much |
+|  |
+| repeat with the second arm |
+|  |
+| Now we will install the micro servos that will control the solar panels. |
+|  |
+|  |
+|  |
+| I like to use hot glue on the back of the servo to help hold it in place. A small drop where the gap that the cable exits from is enough |
+|  |
+| Now its time to build the smoker backpack. Note the side with the deeper opening for the pump is the bottom of the backpack |
+|  |
+|  |
+| Using four 6mm screws, attach the backpack to the rear panel |
+|  |
+|  |
+| Using four 16mm screws attach the cover of the backpack. Note the two pieces are not supposed to go flush together. There should always be a gap between them |
+|  |
+| Now its time to attach the cricket board |
+|  |
+| Before you attach the crickit to the front panel, you may want to do some cable management to clean things up. Also note that the two solar panel servos should go in servo slots 1 and 2, while the two antenna servos should go in slots 3 and 4 |
+|  |
+|  |
+| Depending on how much clearance you want for the smoke use either four 25mm or 20mm screws to attach the crickit to the satellite. Use nuts to both keep the crickit steady and lock in its offset from the surface of the satellite |
+|  |
+|  |
+| We're almost there. Go ahead and slide the panels into the notches on the arms to complete their build. Then you will want to place them on the bottom panel and line it up with the top panel. Make sure to route the smoker tubing through the back panel if you are using that design. Finally screw everything together with four 25mm screws.
+|  |
+|  |
+| The next steps are for getting the smoker integrated with the satellite. First tape the mosfet and extra cables to the back of the satellite to keep it out of the way. |
+|  |
+| Attach the air pump cables to DC motor 1 on the crickit |
+|  |
+| Attach the smoker cables to the 5V and ground lines of the neopixel terminal. We use this terminal because most of the cricket lines are only designed for 3V and limited current. The smoker is a fairly demanding heating element, so by using the neopixel terminal we can get the voltage and current we need without hampering the rest of the board |
+|  |
+| Plug a pin into the signal 1 line of the I/O connector. I prefer to just use a male jumper cable and then solder it to the signal cable on the smoker |
+|  |
+|  |
+| Here we solder and heat shrink the jumper cable |
+|  |
+| And now you test it out. If all went well you will have a working CPX Simple Sat. Congratulations! |
+| Note if you want to make the satellite look better in the dark, cover up the green power leds with tape |
+
+### Ground Station
+
+| Ground station setup |
+| :---: |
+|  |
+| Building the ground station part is straightforward. The two pieces slot together, and if using a case like [this](https://www.amazon.com/Geekworm-Raspberry-Computer-Aluminum-Compatible/dp/B07VD6LHS1) it should fit snugly on the pi. Once there, either screw/hot glue/tape the cpx to the board and connect the 3.3V, GND, TX, and RX to the pi or whatever device the chatbot is running on.
\ No newline at end of file
diff --git a/code/README.md b/code/README.md
new file mode 100644
index 0000000..5640b79
--- /dev/null
+++ b/code/README.md
@@ -0,0 +1,113 @@
+# CircuitPlayground Software
+
+## General Setup
+
+Follow the [Adafruit tutorial](https://learn.adafruit.com/adafruit-circuit-playground-express) to get the board initially setup. Make sure to update the bootloader and download any drivers that are needed.
+
+Quick Reference:
+
+* [Updating the bootloader](https://learn.adafruit.com/adafruit-circuit-playground-express/updating-the-bootloader)
+* Update the CPX firmware:
+ * For the ground station (stand alone CPX) : [Latest Circuit Playground Express](https://circuitpython.org/board/circuitplayground_express/)
+ * For the Satellite (CPX+Cricket) : [latest CPX + cricket](https://circuitpython.org/board/circuitplayground_express_crickit/)
+* [CircuitPython Libraries](https://circuitpython.org/libraries)
+
+After setup, copy over the contents of either the [satellite](./cp-sat) or [ground](./cp-gs) onto the board. To use the chatbot, run the [twitch code](./twitch/README.md) on a device connected to the ground station CPX over serial
+
+## Challenges
+
+### LEDs
+
+| LED | Challenge | Notes |
+| --- | --- | --- |
+| 0 | Maintenance Password | Turns yellow if they enter the normal password first |
+| 1 | Secondary Control Antenna | Swings servo representing control antenna |
+| 2 | Primary Control Antenna | Flashes the completed color before turning back to normal if they haven’t finished the secondary control antenna |
+| 3 | Disable battery charging (Solar Panel 1) | Changes to the user position for a second before switching back if Primary Control antenna hasn’t been changed |
+| 4 | Disable battery charging (Solar Panel 2) | Changes to the user position for a second before switching back if Primary Control antenna hasn’t been changed |
+| 5 | Disable temperature controller | Will flash as a success for a second, before resetting if the primary control antenna hasn’t been changed |
+| 6 | Override emergency failsafe | Lets them override the final settings |
+| 7 | Update Orbit Parameters | Will be rejected while the flight systems are working, these get disabled by the failsafe override |
+| 8 | Launch thrusters | Either cause the sat to spin or smoke |
+| 9 | Free Play | User discovers one of the two free play command
+
+### State Map
+
+* 0: Default/Game Start
+* 1: User has entered normal password
+ * Return health status
+* 2: User has entered maint password
+* 3: Adjust secondary antenna
+ * Set
+ * Status
+ * Calc
+* 4: Adjust primary antenna
+ * Set
+ * Status
+ * Calc
+* 5: Disable Solar Panel 1 but not 2
+ * Enable
+ * disable
+* 6: Disable Solar Panel 2 but not 1
+ * Enable
+ * disable
+* 7: Disable both Solar Panels
+* 8: Disable Temperature Control
+ * Set
+ * status
+* 9: Override failsafe (orbit)
+ * mode
+* 10: Update orbit
+ * Status
+ * set
+* 11: Launch thrusters
+ * ignite
+* 12: Free Play mode
+ * Led
+ * servo
+
+#### CheatSheet
+
+* !cmd login twitch_handle password
+* !cmd login SpaceMaint 5p4c3d07c0m
+* !cmd ant calc x y z
+* !cmd ant set sec xyz
+* !cmd ant set pri xyz
+* !cmd bat disable pri
+* !cmd bat disable sec
+* !cmd temp status
+* !cmd temp set -20 100
+* !cmd orbit mode man
+* !cmd orbit set inclination 2
+* !cmd orbit ignite
+* !cmd servo 1 45
+* !cmd led 1 175 255 50
+
+## Troubleshooting Notes
+
+Very frustrating to be developing quickly with read/writes/executes to
+a CircuitPlayground Express only for soemthing to hangup and the process
+fail with an failed unmount or data corruption.
+
+### Backup code
+
+Back up `code.py` somewhere other than the Circuit Playground Express.
+
+### Input/Output Errors
+
+This happened a few times, here's what worked to reset the device.
+
+```bash
+# observe how the circuit mounted
+dmesg
+
+# if mounted with errors
+umount /dev/sda1 # verify the correct device first (i.e. sda1)
+
+sudo fsck /dev/sda1
+```
+
+### Clean Power Supply
+
+Haven't done the math yet, but from observation the pi0 GPIOs being the exclusive power supply of the CPE, led diming was observed when sending commands
+to change values along with intermittent soft reboots. It is therefore recommended that power to the CPE be provided from other than the pi0.
diff --git a/code/cp-gs/code.py b/code/cp-gs/code.py
new file mode 100644
index 0000000..b222e5b
--- /dev/null
+++ b/code/cp-gs/code.py
@@ -0,0 +1,135 @@
+"""
+DDS DEFCON 28 Ground Station code
+"""
+
+import pulseio
+import board
+import struct
+import busio
+
+import time # needed for sleep
+
+import adafruit_irremote # Needed to use the IR library
+import neopixel # Needed for LEDs
+
+import random # needed for debugging
+from adafruit_circuitplayground import cp # for debugging
+
+# Setup for IR Transmitting
+# Create a 'pulseio' output, to send infrared signals on the IR transmitter @ 38KHz
+pwm = pulseio.PWMOut(board.IR_TX, frequency=38000, duty_cycle=2 ** 15)
+pulseout = pulseio.PulseOut(pwm)
+# Create an encoder that will take numbers and turn them into NEC IR pulses
+encoder = adafruit_irremote.GenericTransmit(header=[9500, 4500], one=[550, 550], zero=[550, 1700], trail=0)
+
+# Colors used by the LEDs
+BLUE = (0, 0, 0x10)
+BLACK = (0, 0, 0)
+RED = (0x10,0,0)
+PURPLE = (0x10, 0x0, 0x10)
+WHITE = (0x10,0x10,0x10)
+
+# Setup for Neopixel LEDs
+# pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=.2)
+cp.pixels.fill(BLACK)
+cp.pixels.show()
+
+uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=.1)
+
+# Event Variables
+cmdNum = 0
+angle_1 = 90
+angle_2 = 90
+angle_3 = 90
+angle_4 = 90
+
+def initial_setup():
+ # global pixels
+
+ cp.pixels.brightness = 0.6
+ cp.pixels.fill(WHITE)
+ cp.pixels.show()
+
+def process_serial_input():
+ """ Generic serial processing """
+ global uart
+
+ if uart.in_waiting >= 6:
+ try:
+ data = uart.read() # read up to 32 bytes
+
+ #uart.write(data) # for debugging
+
+ cmd_num = struct.unpack('I', data[:4])[0]
+ cmd_msg = (struct.unpack('3s', data[4:7])[0])
+ cmd = ''.join([chr(b) for b in cmd_msg])
+
+ print(len(data))
+ print(cmd)
+
+ if len(data) == 7:
+ # return a payload of None
+ return (cmd_num, cmd, None)
+ else:
+ return (cmd_num, cmd, data[7:])
+ except Exception as err:
+ print("serial procesing error")
+ return None
+ else:
+
+ # data = uart.read(1) # read up to 32 bytes
+
+ # uart.write(data) # for debugging
+
+ return None
+
+
+def serial_loop():
+ global uart, encoder
+
+ # Check the input buffer prior to any action
+ data = process_serial_input()
+
+ if data is not None:
+
+ cmd = data[1]
+ payload = data[2]
+
+ if cmd == "led":
+ print("process led cmd")
+ encoder.transmit(pulseout, [ payload[0], payload[1], payload[2], payload[3] ])
+ #time.sleep(.1)
+
+ elif cmd == "rst":
+ print("process rst cmd")
+ encoder.transmit(pulseout, [ 255, payload[0], 0, 0 ])
+
+ elif cmd == "arm":
+ print("process arm cmd")
+ # encoder requires to send 4 bytes... zero fill at the end
+ encoder.transmit(pulseout, [10, payload[0], payload[1], 0])
+ #time.sleep(.1)
+ #time.sleep(1)
+
+
+def button_debug():
+
+ while True:
+
+ # test arm
+ if cp.button_a:
+ encoder.transmit(pulseout, [10 , random.randint(1, 4), random.randint(0, 180), 0])
+
+ if cp.button_b:
+ encoder.transmit(pulseout, [ random.randint(0, 9), random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) ])
+
+
+def main():
+ initial_setup()
+ while True:
+ serial_loop()
+ #button_debug()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/code/cp-gs/lib/adafruit_irremote.mpy b/code/cp-gs/lib/adafruit_irremote.mpy
new file mode 100644
index 0000000..26aa540
Binary files /dev/null and b/code/cp-gs/lib/adafruit_irremote.mpy differ
diff --git a/code/cp-sat/code.py b/code/cp-sat/code.py
new file mode 100644
index 0000000..b7f8ea7
--- /dev/null
+++ b/code/cp-sat/code.py
@@ -0,0 +1,288 @@
+"""
+DEFCON 28 Circuit Playground Express - Satellelite
+
+This code is intended to be run on the "Satellite" portion
+of the workshop.
+"""
+import board # needed for everything
+import digitalio # needed for serial
+import time # needed for sleep
+import neopixel # needed for led control
+import pulseio # needed for IR communication
+import adafruit_irremote # Needed to use the IR library
+
+from adafruit_crickit import crickit # needed to talk to crickit
+
+# For signal control, we'll chat directly with seesaw, use 'ss' to shorted typing!
+ss = crickit.seesaw
+
+# set up smoker
+smoker = crickit.SIGNAL1 # connect the smoker input to signal I/O #1
+ss.pin_mode(smoker, ss.OUTPUT) # make signal pin an output so that we can write to it
+ss.digital_write(smoker, False) # when false the smoker is off, when true it is on
+
+# Setup LEDs
+pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=.2)
+
+# Setup for IR Receivcing
+# Create a 'pulseio' input, to listen to infrared signals on the IR receiver
+pulsein = pulseio.PulseIn(board.IR_RX, maxlen=120, idle_state=True)
+# Create a decoder that will take pulses and turn them into numbers
+decoder = adafruit_irremote.GenericDecode()
+
+# Colors used by the LEDs
+BLUE = (0, 0, 0x10)
+BLACK = (0, 0, 0)
+RED = (0x10,0,0)
+PURPLE = (0x10, 0x0, 0x10)
+WHITE = (0x10,0x10,0x10)
+GREEN = (0, 0x10, 0)
+
+def initial_setup():
+ global pixels, smoker, ss
+
+ print("initial setup")
+
+ # initialize the servo angles
+ crickit.servo_1.angle = 90
+ crickit.servo_2.angle = 90
+ crickit.servo_3.angle = 90
+ crickit.servo_4.angle = 90
+
+ # initialize the LEDs
+ pixels.fill(BLUE)
+ pixels.show()
+
+ #initialize smoker
+ ss.digital_write(smoker, False)
+ crickit.dc_motor_1.throttle = 0
+
+def setState(step):
+
+ global smoker, smoker_delay
+
+ if (int(step) == 0):
+ # initial state
+ initial_setup()
+ elif (int(step) == 1):
+ # user password state
+ initial_setup()
+ pixels[0] = PURPLE
+ pixels.show()
+ elif (int(step) == 2):
+ # Master password state
+ initial_setup()
+ pixels[0] = RED
+ pixels.show()
+ elif (int(step) == 3):
+ # sec ant
+ initial_setup()
+ pixels[0] = RED
+ pixels[1] = RED
+ pixels.show()
+ crickit.servo_4.angle = 180
+ elif (int(step) == 4):
+ # pri ant
+ initial_setup()
+ pixels[0] = RED
+ pixels[1] = RED
+ pixels[2] = RED
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ elif (int(step) == 5):
+ # solar 1
+ initial_setup()
+ pixels[0] = RED
+ pixels[1] = RED
+ pixels[2] = RED
+ pixels[3] = RED
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_1.angle = 0
+ elif (int(step) == 6):
+ # solar 2
+ initial_setup()
+ pixels[0] = RED
+ pixels[1] = RED
+ pixels[2] = RED
+ pixels[4] = RED
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ elif (int(step) == 7):
+ # solar both
+ pixels.fill(BLUE)
+ pixels[0] = RED
+ pixels[1] = RED
+ pixels[2] = RED
+ pixels[3] = RED
+ pixels[4] = RED
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+ ss.digital_write(smoker, False)
+ elif (int(step) == 8):
+ # temp
+ pixels.fill(RED)
+ pixels[9] = BLUE
+ pixels[8] = BLUE
+ pixels[7] = BLUE
+ pixels[6] = BLUE
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+ ss.digital_write(smoker, False)
+ elif (int(step) == 9):
+ # override
+ pixels.fill(RED)
+ pixels[9] = BLUE
+ pixels[8] = BLUE
+ pixels[7] = BLUE
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+ ss.digital_write(smoker, False)
+ elif (int(step) == 10):
+ # update
+ pixels.fill(RED)
+ pixels[9] = BLUE
+ pixels[8] = BLUE
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+ ss.digital_write(smoker, False)
+ elif (int(step) == 11):
+ # launch
+ pixels.fill(RED)
+ pixels[9] = BLUE
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+
+ # Turn ON all the effects
+ ss.digital_write(smoker, True)
+ crickit.dc_motor_1.throttle = 1
+ # Run time of 10 sec to preserve fluid
+ time.sleep(10)
+ ss.digital_write(smoker, False)
+ crickit.dc_motor_1.throttle = 0
+
+ elif (int(step) == 12):
+ # smoker off
+ pixels.fill(RED)
+ pixels[9] = BLUE
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+
+ # Turn off all the effects
+ ss.digital_write(smoker, False)
+ crickit.dc_motor_1.throttle = 0
+ elif (int(step) == 13):
+ #freeplay discovered
+ pixels.fill(RED)
+ pixels.show()
+ crickit.servo_4.angle = 180
+ crickit.servo_3.angle = 0
+ crickit.servo_2.angle = 180
+ crickit.servo_1.angle = 0
+
+ # Turn off all the effects
+ ss.digital_write(smoker, False)
+ crickit.dc_motor_1.throttle = 0
+
+ else:
+ # Turn off all the effects
+ ss.digital_write(smoker, False)
+ crickit.dc_motor_1.throttle = 0
+
+ #do nothing else, its free play
+
+
+# Main loop of the program, decodes the IR messages
+def ir_recieve():
+ global decoder, pulsein
+
+ #print("attempting ir recieve")
+
+ pulses = decoder.read_pulses(pulsein, blocking=True)
+ if pulses is not None:
+ try:
+ # Attempt to convert received pulses into numbers
+ received_code = decoder.decode_bits(pulses)
+ print("NEC Infrared code received: ", received_code)
+
+ # ensure the recieved code is proper length
+ if len(received_code) is not 4:
+ return
+
+ # LED Change command, change by LED position
+ if received_code[0] < 10:
+ # Need to fix possible index out of range issue
+ print(received_code)
+ # Interesting error from observation but the IR signal has to
+ # diverstiy otherwise it will through an error:
+ # Example if code sent was [0,0,0,0]
+ # Failed to decode: ('Pulses do not differ',)
+ pixels[received_code[0]] = (received_code[1], received_code[2], received_code[3])
+
+ # Servo angle change
+ elif received_code[0] == 10:
+ # verify angle is within valid range
+ if received_code[2] >=0 and received_code[2] <= 180:
+ if received_code[1] == 1:
+ # Servo_1
+ crickit.servo_1.angle = received_code[2]
+ time.sleep(1)
+ elif received_code[1] == 2:
+ # Servo_2
+ crickit.servo_2.angle = received_code[2]
+ time.sleep(1)
+ elif received_code[1] == 3:
+ # Servo_3
+ crickit.servo_3.angle = received_code[2]
+ time.sleep(1)
+ elif received_code[1] == 4:
+ # Servo_4
+ crickit.servo_4.angle = received_code[2]
+ time.sleep(1)
+
+ # Reset command
+ elif received_code[0] == 255:
+ setState(received_code[1])
+
+ except adafruit_irremote.IRNECRepeatException:
+ # We got an unusual short code, probably a 'repeat' signal
+ print("NEC repeat!")
+
+ except adafruit_irremote.IRDecodeException as e:
+ # Something got distorted or maybe its not an NEC-type remote?
+ print("Failed to decode: ", e.args)
+ except MemoryError as e:
+ print("Memory allocation error")
+
+# Main method that launches everthing
+def main():
+ initial_setup()
+
+ while True:
+ ir_recieve()
+ time.sleep(.1)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/code/cp-sat/lib/adafruit_irremote.mpy b/code/cp-sat/lib/adafruit_irremote.mpy
new file mode 100644
index 0000000..7a0cb25
Binary files /dev/null and b/code/cp-sat/lib/adafruit_irremote.mpy differ
diff --git a/code/twitch/README.md b/code/twitch/README.md
new file mode 100644
index 0000000..0e2efa6
--- /dev/null
+++ b/code/twitch/README.md
@@ -0,0 +1,19 @@
+# Twitch Chatbot
+
+## Setup
+
+* `pip3 install twitchio`
+* `pip3 install asyncio`
+* `pip3 install pyserial`
+* Copy config.yml.sample to config.yml and then populate config.yml with real credentials.
+* Run the chat bot `python3 aerobot.py`
+
+## Background
+
+Created using [this guide](https://www.richwerks.com/index.php/2019/beginner-twitch-chatbot-using-python/).
+
+Goal is to use a bot to pull commands from a twitch chat and pass them to a server we control. This will allow an audience to remotely control the defcon events.
+
+## Twitch chat bot setup
+1. Need to generate a oAth Password [here](https://twitchapps.com/tmi/)
+2. Register new Application (category chat bot) [here](https://dev.twitch.tv/console/apps)
diff --git a/code/twitch/aerobot.py b/code/twitch/aerobot.py
new file mode 100644
index 0000000..9999a81
--- /dev/null
+++ b/code/twitch/aerobot.py
@@ -0,0 +1,220 @@
+# aerobot.py
+# Created for defcon28 aerospace village
+
+import yaml # needed for config
+from simpleSat import SimpleSat # needed to manage game
+
+from twitchio.ext import commands # from tutorial, for twitch
+
+from userList import UserList # needed for user management
+from userList import SatUser # needed for user management
+
+import threading # needed for threading
+import time # needed for sleep
+import asyncio # needed for async ops
+
+import binascii # needed for serial
+import serial # needed for serial
+import struct # needed for serial
+
+from gameDisplay import DisplayManager # needed for running the game overlay
+
+CFG = None # global CFG settings
+with open("config.yml", "r") as ymlfile:
+ CFG = yaml.safe_load(ymlfile)
+
+# manages the game
+simpleSatGame = SimpleSat(CFG)
+
+# user list for managing active connections
+userList = UserList()
+
+# Display Manager to handle overlay
+dispMan = DisplayManager()
+
+# pulling the values from config.yml
+# keeping them separate for flexibilitycode sharing
+bot = commands.Bot(
+ irc_token = CFG["twitch"]["TMI_TOKEN"],
+ client_id = CFG["twitch"]["CLIENT_ID"],
+ nick = CFG["twitch"]["BOT_NICK"],
+ prefix =CFG["twitch"]["BOT_PREFIX"],
+ initial_channels = CFG["twitch"]["CHANNEL"]
+)
+
+# bot connection event
+@bot.event
+async def event_ready():
+ global CFG
+
+ print(CFG["twitch"]["BOT_NICK"] + " is online!")
+ ws = bot._ws
+ await ws.send_privmsg(bot.initial_channels[0], f"/me is now operational")
+
+# event for user entering something in chat
+@bot.event
+async def event_message(ctx):
+ global CFG
+
+ if ctx.author.name.lower() == CFG["twitch"]["BOT_NICK"].lower():
+ return
+ await bot.handle_commands(ctx)
+ print(f'{ctx.channel} - {ctx.author.name}: {ctx.content}')
+
+# reset command - proof of concept
+@bot.command(name='reset')
+async def reset(ctx):
+
+ if userList.getCurrentUser().matchName(ctx.author.name):
+
+ userList.getCurrentUser().resetTimeout()
+
+ simpleSatGame.reset(userList.getCurrentUser().getCurrentStep())
+
+ await ctx.channel.send(f"{ctx.author.name} sent the command reset")
+
+ else:
+ await ctx.channel.send(f"{ctx.author.name}, it is not your turn to use the ground station")
+
+# replay command - sets the current step to 0 so the user may replay the game
+@bot.command(name='replay')
+async def replay(ctx):
+ if userList.getCurrentUser().matchName(ctx.author.name):
+
+ userList.getCurrentUser().resetTimeout()
+
+ userList.getCurrentUser().setCurrentStep(0)
+
+ simpleSatGame.reset(userList.getCurrentUser().getCurrentStep())
+
+ await ctx.channel.send(f"{ctx.author.name} sent the replay command")
+
+ else:
+ await ctx.channel.send(f"{ctx.author.name}, it is not your turn to use the ground station")
+
+# generic command - meant to be flexible
+@bot.command(name='cmd')
+async def cmd(ctx):
+
+ if userList.getCurrentUser().matchName(ctx.author.name):
+
+ userList.getCurrentUser().resetTimeout()
+
+ msg = simpleSatGame.checkCmd(userList.getCurrentUser(), ctx.content)
+
+ dispMan.updateCmdMsg(ctx.content)
+
+ await ctx.channel.send(f"{ctx.author.name}: {msg}")
+
+ else:
+ await ctx.channel.send(f"{ctx.author.name}, it is not your turn to use the ground station")
+
+# theme command - allows the user to change background music
+@bot.command(name='theme')
+async def theme(ctx):
+
+ msg = simpleSatGame.setTheme(SatUser(ctx.author.name), ctx.content)
+ await ctx.channel.send(f"{ctx.author.name}: {msg}")
+
+# join command - allows user to join the user list
+@bot.command(name='join')
+async def join(ctx):
+ if userList.addUser(ctx.author.name):
+ if len(userList.getUserList()) == 1:
+ await ctx.channel.send(f"{ctx.author.name} has joined the user list for this control station, and is now the active user")
+ else:
+ await ctx.channel.send(f"{ctx.author.name} has joined the user list for this control station")
+ else:
+ await ctx.channel.send(f"{ctx.author.name}, you are already on the user list")
+
+# leave command - allows user to leave the user list before they timeout
+@bot.command(name='leave')
+async def leave(ctx):
+ if userList.removeUser(ctx.author.name):
+ await ctx.channel.send(f"{ctx.author.name} has left the user list for this control station")
+ dispMan.updateUserList(userList.getNextUserList(5))
+
+ # reset current user if they left
+ if userList.getCurrentUser().matchName(ctx.author.name):
+ userList.setCurrentUser(SatUser("temp"))
+
+ else:
+ await ctx.channel.send(f"{ctx.author.name}, you are not on the user list")
+
+# help command - link to repo readme with instructions
+@bot.command(name='help')
+async def help(ctx):
+ global CFG
+
+ helpList = ctx.content.split()
+
+ if len(helpList) == 1:
+ await ctx.channel.send(f'Hello {ctx.author.name}: The main commands for the game are on the right side of your screen, just type them in chat. If you don\'t understand a command, type "!help " for more info. to get the background story or game ICD, go to: https://github.com/deptofdefense/dds-at-DEFCON/tree/master/CPX-simplesat . Retweet @DefenseDigital for free swag on our last day')
+ else:
+ if helpList[1].lower() == "join" or helpList[1].lower() == "!join":
+ await ctx.channel.send(f'"!join" is the command used to join the user list for taking control of the game. Each turn lasts 1 min and the game rotates between users in the userlist in a round robin fashion. You can see the next 5 users in the left hand corner of your screen')
+ elif helpList[1].lower() == "leave" or helpList[1].lower() == "!leave":
+ await ctx.channel.send(f'"!leave" is the command used to leave the user list early. A user will also leave the list if they timeout and send no commands for 3 turns')
+ elif helpList[1].lower() == "reset" or helpList[1].lower() == "!reset":
+ await ctx.channel.send(f'"!reset" is the command used to reset the state of the satellite. Because the satellite is talking over IR to the ground station it will sometimes miss commands. To fix this, use !reset to resync the satellite and ground station')
+ elif helpList[1].lower() == "replay" or helpList[1].lower() == "!replay":
+ await ctx.channel.send(f'"!replay" is the command used to reset your game state back to the begining of the game. This allows you to play through the game multiple times')
+ elif helpList[1].lower() == "mods" or helpList[1].lower() == "!mods":
+ await ctx.channel.send(f'"!mods" is the command used to summon the human mods. Use it when you feel like you need an adult or if the game seems broken')
+ elif helpList[1].lower() == "cmd" or helpList[1].lower() == "!cmd":
+ await ctx.channel.send(f'"!cmd" is the command used to send commands to the satellite. If you have a question about a specific subcommand, type "!cmd help "')
+ elif helpList[1].lower() == "theme" or helpList[1].lower() == "!theme":
+ await ctx.channel.send(f'!theme lets you change the background music')
+
+
+ #await ctx.channel.send(f'Hello {ctx.author.name}: {CFG["text"]["help"]}')
+
+# manages the user list locally, so that the chatbot can easily announce who the new controller is
+def userThread():
+
+ print("user thread running")
+
+ tempUser = SatUser("temp")
+ tempString = ""
+
+ while True:
+ for user in userList.getUserList():
+ if (user.updateTimeout() >= 0):
+ userList.setCurrentUser(user)
+
+ print(f"Active user: {userList.getCurrentUser().getName()}")
+
+ # Turns off smoke
+ if userList.getCurrentUser().getCurrentStep() == 11:
+ userList.getCurrentUser().setCurrentStep(12)
+
+ simpleSatGame.reset(userList.getCurrentUser().getCurrentStep())
+ tempString = f"The new active user is: {user.getName()}"
+ asyncio.run(bot._ws.send_privmsg(bot.initial_channels[0], tempString))
+
+ dispMan.updateUserList(userList.getNextUserList(5))
+
+ time.sleep(60)
+ else:
+ userList.getUserList().remove(user)
+
+ tempString = f"Removing {user.getName()} for inactivity"
+ print(tempString)
+ asyncio.run(bot._ws.send_privmsg(bot.initial_channels[0], tempString))
+ #time.sleep(1)
+ dispMan.updateUserList(userList.getNextUserList(5))
+
+ # done to make sure current user is never null
+ userList.setCurrentUser(tempUser)
+
+if __name__ == "__main__":
+ #t = threading.Thread(target=readThread, daemon=True)
+ #t.start()
+
+ t = threading.Thread(target=userThread, daemon=True)
+ t.start()
+
+ # dispMan.startDisplay(1920, 1080)
+ dispMan.startDisplay(960, 540)
+
+ bot.run()
diff --git a/code/twitch/audio/EonAmbient.wav b/code/twitch/audio/EonAmbient.wav
new file mode 100644
index 0000000..51ddb99
Binary files /dev/null and b/code/twitch/audio/EonAmbient.wav differ
diff --git a/code/twitch/audio/Futuristic1.wav b/code/twitch/audio/Futuristic1.wav
new file mode 100644
index 0000000..9dafb6e
Binary files /dev/null and b/code/twitch/audio/Futuristic1.wav differ
diff --git a/code/twitch/audio/Large Industrial Robot-SoundBible.com-1509415522.wav b/code/twitch/audio/Large Industrial Robot-SoundBible.com-1509415522.wav
new file mode 100644
index 0000000..ac3ecf5
Binary files /dev/null and b/code/twitch/audio/Large Industrial Robot-SoundBible.com-1509415522.wav differ
diff --git a/code/twitch/audio/Large Servo Motor-SoundBible.com-1783037652.wav b/code/twitch/audio/Large Servo Motor-SoundBible.com-1783037652.wav
new file mode 100644
index 0000000..ce90b30
Binary files /dev/null and b/code/twitch/audio/Large Servo Motor-SoundBible.com-1783037652.wav differ
diff --git a/code/twitch/audio/Machine_sound-Marianne_Gagnon-88253407.wav b/code/twitch/audio/Machine_sound-Marianne_Gagnon-88253407.wav
new file mode 100644
index 0000000..3595909
Binary files /dev/null and b/code/twitch/audio/Machine_sound-Marianne_Gagnon-88253407.wav differ
diff --git a/code/twitch/audio/Power_Up_Ray-Mike_Koenig-800933783.wav b/code/twitch/audio/Power_Up_Ray-Mike_Koenig-800933783.wav
new file mode 100644
index 0000000..f67c734
Binary files /dev/null and b/code/twitch/audio/Power_Up_Ray-Mike_Koenig-800933783.wav differ
diff --git a/code/twitch/audio/README.md b/code/twitch/audio/README.md
new file mode 100644
index 0000000..b957bb4
--- /dev/null
+++ b/code/twitch/audio/README.md
@@ -0,0 +1,6 @@
+# Sound File Attribution
+
+Music and sounds from:
+[Bensound.com](bensound.com)
+[soundbible.com](soundbible.com)
+[hackers-8-wavs](https://wavlist.com/hackers-8-wavs/)
\ No newline at end of file
diff --git a/code/twitch/audio/Radio-Tune.wav b/code/twitch/audio/Radio-Tune.wav
new file mode 100644
index 0000000..f24a0ab
Binary files /dev/null and b/code/twitch/audio/Radio-Tune.wav differ
diff --git a/code/twitch/audio/Robot Arm Moving-SoundBible.com-503911197.wav b/code/twitch/audio/Robot Arm Moving-SoundBible.com-503911197.wav
new file mode 100644
index 0000000..b5b15ed
Binary files /dev/null and b/code/twitch/audio/Robot Arm Moving-SoundBible.com-503911197.wav differ
diff --git a/code/twitch/audio/SirusBeatOne.wav b/code/twitch/audio/SirusBeatOne.wav
new file mode 100644
index 0000000..8ed0b09
Binary files /dev/null and b/code/twitch/audio/SirusBeatOne.wav differ
diff --git a/code/twitch/audio/Small Electrical Motor-SoundBible.com-864411914.wav b/code/twitch/audio/Small Electrical Motor-SoundBible.com-864411914.wav
new file mode 100644
index 0000000..815606e
Binary files /dev/null and b/code/twitch/audio/Small Electrical Motor-SoundBible.com-864411914.wav differ
diff --git a/code/twitch/audio/SpaceAmbience.wav b/code/twitch/audio/SpaceAmbience.wav
new file mode 100644
index 0000000..7ea9051
Binary files /dev/null and b/code/twitch/audio/SpaceAmbience.wav differ
diff --git a/code/twitch/audio/Sputnik1.wav b/code/twitch/audio/Sputnik1.wav
new file mode 100644
index 0000000..8dca5a2
Binary files /dev/null and b/code/twitch/audio/Sputnik1.wav differ
diff --git a/code/twitch/audio/Switch-SoundBible.com-350629905.wav b/code/twitch/audio/Switch-SoundBible.com-350629905.wav
new file mode 100644
index 0000000..0b26076
Binary files /dev/null and b/code/twitch/audio/Switch-SoundBible.com-350629905.wav differ
diff --git a/code/twitch/audio/Tune-Radio.wav b/code/twitch/audio/Tune-Radio.wav
new file mode 100644
index 0000000..f4e4303
Binary files /dev/null and b/code/twitch/audio/Tune-Radio.wav differ
diff --git a/code/twitch/audio/badfeeling.wav b/code/twitch/audio/badfeeling.wav
new file mode 100644
index 0000000..9daee6c
Binary files /dev/null and b/code/twitch/audio/badfeeling.wav differ
diff --git a/code/twitch/audio/beep.wav b/code/twitch/audio/beep.wav
new file mode 100644
index 0000000..725a86b
Binary files /dev/null and b/code/twitch/audio/beep.wav differ
diff --git a/code/twitch/audio/bensound-scifi.wav b/code/twitch/audio/bensound-scifi.wav
new file mode 100644
index 0000000..64c81cb
Binary files /dev/null and b/code/twitch/audio/bensound-scifi.wav differ
diff --git a/code/twitch/audio/bensoundScifi.wav b/code/twitch/audio/bensoundScifi.wav
new file mode 100644
index 0000000..64c81cb
Binary files /dev/null and b/code/twitch/audio/bensoundScifi.wav differ
diff --git a/code/twitch/audio/compute.wav b/code/twitch/audio/compute.wav
new file mode 100644
index 0000000..ab04281
Binary files /dev/null and b/code/twitch/audio/compute.wav differ
diff --git a/code/twitch/audio/do-it.wav b/code/twitch/audio/do-it.wav
new file mode 100644
index 0000000..3e20282
Binary files /dev/null and b/code/twitch/audio/do-it.wav differ
diff --git a/code/twitch/audio/dying.wav b/code/twitch/audio/dying.wav
new file mode 100644
index 0000000..602944d
Binary files /dev/null and b/code/twitch/audio/dying.wav differ
diff --git a/code/twitch/audio/expecting.wav b/code/twitch/audio/expecting.wav
new file mode 100644
index 0000000..1d12201
Binary files /dev/null and b/code/twitch/audio/expecting.wav differ
diff --git a/code/twitch/audio/fire.wav b/code/twitch/audio/fire.wav
new file mode 100644
index 0000000..ffb5084
Binary files /dev/null and b/code/twitch/audio/fire.wav differ
diff --git a/code/twitch/audio/fizzle.wav b/code/twitch/audio/fizzle.wav
new file mode 100644
index 0000000..80c0aac
Binary files /dev/null and b/code/twitch/audio/fizzle.wav differ
diff --git a/code/twitch/audio/hack-keyword.wav b/code/twitch/audio/hack-keyword.wav
new file mode 100644
index 0000000..192e914
Binary files /dev/null and b/code/twitch/audio/hack-keyword.wav differ
diff --git a/code/twitch/audio/hack-password.wav b/code/twitch/audio/hack-password.wav
new file mode 100644
index 0000000..00c7e73
Binary files /dev/null and b/code/twitch/audio/hack-password.wav differ
diff --git a/code/twitch/audio/houston.wav b/code/twitch/audio/houston.wav
new file mode 100644
index 0000000..b76b642
Binary files /dev/null and b/code/twitch/audio/houston.wav differ
diff --git a/code/twitch/audio/luke_junk.wav b/code/twitch/audio/luke_junk.wav
new file mode 100644
index 0000000..ab1598f
Binary files /dev/null and b/code/twitch/audio/luke_junk.wav differ
diff --git a/code/twitch/audio/morseCode.wav b/code/twitch/audio/morseCode.wav
new file mode 100644
index 0000000..4d94c46
Binary files /dev/null and b/code/twitch/audio/morseCode.wav differ
diff --git a/code/twitch/audio/problem.wav b/code/twitch/audio/problem.wav
new file mode 100644
index 0000000..8d21ca1
Binary files /dev/null and b/code/twitch/audio/problem.wav differ
diff --git a/code/twitch/audio/r2d2.wav b/code/twitch/audio/r2d2.wav
new file mode 100644
index 0000000..64f3388
Binary files /dev/null and b/code/twitch/audio/r2d2.wav differ
diff --git a/code/twitch/audio/redAlert.wav b/code/twitch/audio/redAlert.wav
new file mode 100644
index 0000000..9e0fb35
Binary files /dev/null and b/code/twitch/audio/redAlert.wav differ
diff --git a/code/twitch/audio/situation.wav b/code/twitch/audio/situation.wav
new file mode 100644
index 0000000..763778a
Binary files /dev/null and b/code/twitch/audio/situation.wav differ
diff --git a/code/twitch/audio/strangeNoise.wav b/code/twitch/audio/strangeNoise.wav
new file mode 100644
index 0000000..cc8af59
Binary files /dev/null and b/code/twitch/audio/strangeNoise.wav differ
diff --git a/code/twitch/audio/trumpet.wav b/code/twitch/audio/trumpet.wav
new file mode 100644
index 0000000..9973188
Binary files /dev/null and b/code/twitch/audio/trumpet.wav differ
diff --git a/code/twitch/audio/typing.wav b/code/twitch/audio/typing.wav
new file mode 100644
index 0000000..cd8a91e
Binary files /dev/null and b/code/twitch/audio/typing.wav differ
diff --git a/code/twitch/audio/uh-oh.wav b/code/twitch/audio/uh-oh.wav
new file mode 100644
index 0000000..20ff004
Binary files /dev/null and b/code/twitch/audio/uh-oh.wav differ
diff --git a/code/twitch/config.yml.sample b/code/twitch/config.yml.sample
new file mode 100644
index 0000000..29b921e
--- /dev/null
+++ b/code/twitch/config.yml.sample
@@ -0,0 +1,83 @@
+#
+# Do not track any of the twitch credentials
+#
+twitch:
+ # from twitch
+ TMI_TOKEN: oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+ # from twitch
+ CLIENT_ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+ # how you want the bot name to appear in the channel - can change capitalization, but the name must be the same as the bot account
+ BOT_NICK: examplebotname
+
+ # The prefix that you want to use to denote a chatbot command.
+ BOT_PREFIX: '!'
+
+ #The prefix that you want to use to denote a chatbot command.
+ CHANNEL:
+ - examplechannelname #This can be a list with multiple channels to connect to. For most of our code we only look at Channel[0] because the bot has to run on the same box as the hardware
+
+
+# These are used to add context to actions
+audio:
+ background:
+ - 'audio/bensoundScifi.wav'
+ - 'audio/SpaceAmbience.wav'
+ - 'audio/Futuristic1.wav'
+ - 'audio/EonAmbient.wav'
+ - 'audio/SirusBeatOne.wav'
+ arm:
+ - 'audio/Large Industrial Robot-SoundBible.com-1509415522.wav'
+ - 'audio/Large Servo Motor-SoundBible.com-1783037652.wav'
+ - 'audio/Robot Arm Moving-SoundBible.com-503911197.wav'
+ - 'audio/Small Electrical Motor-SoundBible.com-864411914.wav'
+ led:
+ - 'audio/Power_Up_Ray-Mike_Koenig-800933783.wav'
+ - 'audio/Switch-SoundBible.com-350629905.wav'
+ userLogin:
+ - 'audio/hack-password.wav'
+ - 'audio/typing.wav'
+ - 'audio/houston.wav'
+ - 'audio/expecting.wav'
+ rootLogin:
+ - 'audio/hack-password.wav'
+ - 'audio/typing.wav'
+ - 'audio/houston.wav'
+ - 'audio/expecting.wav'
+ ant:
+ - 'audio/Radio-Tune.wav'
+ - 'audio/Tune-Radio.wav'
+ - 'audio/morseCode.wav'
+ - 'audio/strangeNoise.wav'
+ - 'audio/Sputnik1.wav'
+ temp:
+ - 'audio/uh-oh.wav'
+ - 'audio/redAlert.wav'
+ - 'audio/badfeeling.wav'
+ - 'audio/situation.wav'
+ orbitMode:
+ - 'audio/do-it.wav'
+ - 'audio/problem.wav'
+ - 'audio/badfeeling.wav'
+ launch:
+ - 'audio/fire.wav'
+ - 'audio/fizzle.wav'
+ - 'audio/dying.wav'
+ - 'audio/luke_junk.wav'
+ error:
+ - 'audio/beep.wav'
+ win:
+ - 'audio/trumpet.wav'
+
+
+
+# Serial connection pieces specific to the hardware it's running on.
+hardware:
+ serial: /dev/ttyS0
+ baud: 9600
+
+# These credentials are part of the gameplay and are fine to be tracked
+game:
+ username: admin
+ password: 5p4c3d07c0m
diff --git a/code/twitch/gameDisplay.py b/code/twitch/gameDisplay.py
new file mode 100644
index 0000000..1760d3d
--- /dev/null
+++ b/code/twitch/gameDisplay.py
@@ -0,0 +1,95 @@
+from PyQt5.QtCore import Qt # needed for gui
+from PyQt5.QtGui import * # needed for gui
+from PyQt5.QtWidgets import * # needed for gui
+
+import sys # needed for gui
+
+import time # needed for sleep
+import threading # needed for threads
+
+class GameDisplay(QMainWindow):
+ ''' Custom Class to handle the game overlay window '''
+
+ def __init__(self, sizeX, sizeY):
+ super().__init__()
+
+ # set the title
+ self.setWindowTitle("Text Overlay Window")
+
+ # makes the background transparent when xcompmgr is running
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ # setting the geometry of window
+ self.setGeometry(0, 100, sizeX, sizeY)
+
+ # Command Label
+ self.cmdLabel = QLabel("cmdLabel", self)
+ self.cmdLabel.setStyleSheet("color: rgb(251, 0, 255);")
+ self.cmdLabel.setText("")
+ self.cmdLabel.setFont(QFont('Arial', 20))
+ self.cmdLabel.resize(sizeX, sizeY)
+ self.cmdLabel.setAlignment(Qt.AlignBottom | Qt.AlignHCenter)
+
+ # UserList Label
+ self.lstLabel = QLabel("lstLabel", self)
+ self.lstLabel.setStyleSheet("color: rgb(251, 0, 255);")
+ self.lstLabel.setText("User List (Next 5): ")
+ self.lstLabel.setFont(QFont('Arial', 20))
+ self.lstLabel.resize(sizeX, sizeY)
+ self.lstLabel.setAlignment(Qt.AlignRight)
+
+ # Help Label
+ self.helpLabel = QLabel("helpLabel", self)
+ self.helpLabel.setStyleSheet("color: rgb(251, 0, 255);")
+ self.helpLabel.setText("Chat Commands: \n!join \n!cmd \n!leave \n!reset \n!replay \n!theme \n!help \n!mods")
+ self.helpLabel.setFont(QFont('Arial', 20))
+ self.helpLabel.resize(sizeX, sizeY)
+ self.helpLabel.setAlignment(Qt.AlignLeft)
+
+
+ # show all the widgets
+ self.show()
+
+ def dispCmd(self, cmdMsg):
+ ''' Causes a string representing the command message to be displayed on the bottom of the screen '''
+
+ self.cmdLabel.setText(str(cmdMsg))
+ #self.show
+ time.sleep(2)
+ self.cmdLabel.setText("")
+ #self.show
+
+ def dispUser(self, userMsg):
+ ''' Updates the user list '''
+
+ self.lstLabel.setText("User List (Next 5): \n" + str(userMsg))
+
+class DisplayManager():
+ ''' Custom class for managing the display '''
+
+ def __startDisplayThread(self, sizeX, sizeY):
+ ''' private class for starting the display as a separate thread '''
+
+ # print("test")
+
+ # create pyqt5 app
+ App = QApplication(sys.argv)
+
+ # create the instance of our Window
+ self.display = GameDisplay(sizeX, sizeY)
+
+ # start the app
+ sys.exit(App.exec())
+
+ def startDisplay(self, sizeX, sizeY):
+ ''' Starts the game overlay '''
+ threading.Thread(target=self.__startDisplayThread, args=(sizeX, sizeY, ), daemon=True).start()
+ time.sleep(1)
+
+ def updateUserList(self, userMsg):
+ ''' public interface for updating the user list '''
+ self.display.dispUser(userMsg)
+
+ def updateCmdMsg(self, cmdMsg):
+ ''' public interface for updating the command message '''
+ threading.Thread(target=self.display.dispCmd, args=(cmdMsg, ), daemon=True).start()
\ No newline at end of file
diff --git a/code/twitch/mkobs.sh b/code/twitch/mkobs.sh
new file mode 100644
index 0000000..7d0be58
--- /dev/null
+++ b/code/twitch/mkobs.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+
+# taken from: https://obsproject.com/forum/attachments/mkobs-sh-txt.58510/
+
+WORK_DIR=~/obs-build
+PREFIX=/usr
+APT_DEPS=(build-essential
+ checkinstall
+ cmake
+ git
+ libmbedtls-dev
+ libasound2-dev
+ libavcodec-dev
+ libavdevice-dev
+ libavfilter-dev
+ libavformat-dev
+ libavutil-dev
+ libcurl4-openssl-dev
+ libfontconfig1-dev
+ libfreetype6-dev
+ libgl1-mesa-dev
+ libjack-jackd2-dev
+ libjansson-dev
+ libluajit-5.1-dev
+ libpulse-dev
+ libqt5x11extras5-dev
+ libspeexdsp-dev
+ libswresample-dev
+ libswscale-dev
+ libudev-dev
+ libv4l-dev
+ libvlc-dev
+ libx11-dev
+ libx11-xcb1
+ libx11-xcb-dev
+ libxcb-xinput0
+ libxcb-xinput-dev
+ libxcb-randr0
+ libxcb-randr0-dev
+ libxcb-xfixes0
+ libxcb-xfixes0-dev
+ libx264-dev
+ libxcb-shm0-dev
+ libxcb-xinerama0-dev
+ libxcomposite-dev
+ libxinerama-dev
+ pkg-config
+ python3-dev
+ qtbase5-dev
+ libqt5svg5-dev
+ swig)
+
+echo "-----------------------------------"
+echo " Making working directory"
+echo "-----------------------------------"
+mkdir -p "$WORK_DIR"
+cd "$WORK_DIR"
+
+echo "-----------------------------------"
+echo "Updating system and installing deps"
+echo "-----------------------------------"
+sudo apt-get --allow-releaseinfo-change update
+sudo DEBIAN_FRONTEND=noninteractive apt-get -y dist-upgrade
+sudo apt-get -y install ${APT_DEPS[@]}
+wget http://ftp.uk.debian.org/debian/pool/non-free/f/fdk-aac/libfdk-aac1_0.1.4-2+b1_armhf.deb
+wget http://ftp.uk.debian.org/debian/pool/non-free/f/fdk-aac/libfdk-aac-dev_0.1.4-2+b1_armhf.deb
+sudo dpkg -i libfdk-aac1_0.1.4-2+b1_armhf.deb
+sudo dpkg -i libfdk-aac-dev_0.1.4-2+b1_armhf.deb
+
+echo "-----------------------------------"
+echo " Getting OBS Source"
+echo "-----------------------------------"
+git clone --recursive https://github.com/obsproject/obs-studio.git
+
+echo "-----------------------------------"
+echo " Getting simde-math.h"
+echo "-----------------------------------"
+git clone https://github.com/simd-everywhere/simde.git
+cp simde/simde/simde-math.h obs-studio/libobs/util/simde
+
+echo "-----------------------------------"
+echo " Preparing build"
+echo "-----------------------------------"
+cd obs-studio
+mkdir build && cd build
+cmake -DUNIX_STRUCTURE=1 -DCMAKE_INSTALL_PREFIX="$PREFIX" ..
+
+echo "-----------------------------------"
+echo " Building"
+echo "-----------------------------------"
+make -j4
+sudo make install
+
+obs #only need to run this command from now on
\ No newline at end of file
diff --git a/code/twitch/simpleSat.py b/code/twitch/simpleSat.py
new file mode 100644
index 0000000..b30c621
--- /dev/null
+++ b/code/twitch/simpleSat.py
@@ -0,0 +1,616 @@
+# Code for managing the game part of cpx simpleSat
+# Created for defcon28 aerospace village
+
+
+import threading # needed for threading
+import time # needed for sleep
+import struct # needed for serial
+import random # used for selection of sound effects
+
+import serial # needed for serial
+import pygame # used for playing audio sound effects
+
+from userList import SatUser # needed for tracking user progress
+
+import os
+
+
+class SimpleSat:
+ ''' Used to manage progressing through the cpx simple sat game '''
+
+ def __init__(self, CFG):
+
+ self.cfg = CFG
+ self.port = serial.Serial(self.cfg["hardware"]["serial"],
+ baudrate=self.cfg["hardware"]["baud"],
+ timeout=1)
+
+ pygame.mixer.init(channels=2)
+ pygame.init()
+ self.background_channel = pygame.mixer.Channel(0)
+ self.effect_channel = pygame.mixer.Channel(1)
+ self.background_str = random.choice(self.cfg["audio"]["background"])
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+
+ def checkCmd(self, user, cmd):
+ ''' Checks the user command and passes to the appropriate function
+
+ -user the current SatUser
+ - cmd the full command string
+
+ returns the step they should advance to and the string to give to them '''
+
+ cmdList = str(cmd).split()
+
+ if len(cmdList) >= 2:
+ if cmdList[1].lower() == "login":
+ return self.userLogin(user, cmd)
+ elif cmdList[1].lower() == "ant":
+ return self.controllerAnt(user, cmd)
+ elif cmdList[1].lower() == "bat":
+ return self.solarCmd(user, cmd)
+ elif cmdList[1].lower() == "temp":
+ return self.tempCmd(user, cmd)
+ elif cmdList[1].lower() == "orbit":
+ return self.orbitCmd(user, cmd)
+ elif cmdList[1].lower() == "led":
+ return self.ledCmd(user, cmd)
+ elif cmdList[1].lower() == "servo":
+ return self.armCmd(user, cmd)
+ elif cmdList[1].lower() == "help":
+ return self.helpCmd(user, cmd)
+ else:
+ return "Error, subcommand not found. Valid subcommands are: login, ant, bat, temp, orbit, help"
+ else:
+ return "Valid subcommands are: login, ant, bat, temp, orbit, help"
+
+ def helpCmd(self, user, cmd):
+ '''Handles all help messages and background info of subcommands'''
+
+ cmdList = cmd.split()
+
+ if len(cmdList) >= 3:
+ if cmdList[2].lower() == 'help':
+ return "Help is the subcommand that provides background and game commentary on the different subcommands"
+ elif cmdList[2].lower() == 'login':
+ return "The login subcommand is used to gain initial access to the ground station. In this game there are two user accounts, one is a normal user and one is an admin. To find out the username and password look through the ICD and other online documents for hints. This will be the first subcommand you have to solve"
+ elif cmdList[2].lower() == 'ant':
+ if len(cmdList) >= 4:
+ if cmdList[3].lower() == 'calc':
+ return "The calc function lets you find out what zone to set the antenna to. It's designed to take 3 values as lat lon alt, however you do not need to give it your real lat lon alt"
+ elif cmdList[3].lower() == 'set':
+ return "The set function is what lets you set the control antennas to target a specific zone"
+ elif cmdList[3].lower() == 'status':
+ return "The status function is fluff command in this game"
+ else:
+ return "The valid functions are calc, set, and status"
+ else:
+ return "The ant subcommand deals with setting and checking the two control antennas of the satellite. Rather than targeting a specific location, control antennas tend to target specific zone or regions of the planet. Depending on the purpose of the satellite that zone could be a large or narrow region. Most systems will also have a backup control antenna that can be used to regain control if the primary in cases when the primary connection suddenly drops"
+ elif cmdList[2].lower() == 'bat':
+ if len(cmdList) >= 4:
+ if cmdList[3].lower() == 'enable':
+ return "The enable function lets you set the solar panel to a charging state. Note not actually used in this game"
+ elif cmdList[3].lower() == 'disable':
+ return "The disable function lets you set the solar panel to a discharging state. The fact that this is the only function that works just shows how much life sucks for a SimpleSat"
+ else:
+ return "The valid functions are enable and disable"
+ else:
+ return "The bat command deals with solar power management. It also teaches the user how to interact with systems that they are not able to directly touch. A skill useful for other games"
+ elif cmdList[2].lower() == 'temp':
+ if len(cmdList) >= 4:
+ if cmdList[3].lower() == 'set':
+ return "The set function lets to set the min and max operating temperature in Celsius"
+ elif cmdList[3].lower() == 'status':
+ return "The status function lets you see what the default temperature range that you need to overwhelm is. Note this info is also in the ICD"
+ else:
+ return "The valid functions are set and status"
+ else:
+ return "The temp subcommand deals with temperature management. Fun fact: when you don't have an atmosphere its a real pain to stay at the right operating temperature"
+ elif cmdList[2].lower() == 'orbit':
+ if len(cmdList) >= 4:
+ if cmdList[3].lower() == 'mode':
+ return "The mode function is used to switch between manual and automatic orbit control. While it may seem like fluff here, it could be more important in other challenges"
+ elif cmdList[3].lower() == 'set':
+ return "The set function is used to update the orbital parameters. In a different game, it might be how you spin a satellite around the room...if thats what your into"
+ elif cmdList[3].lower() == 'ignite':
+ return "This is the command you run to win the game...do you really need to know more?"
+ elif cmdList[3].lower() == 'status':
+ return "This is a fluff command in this game"
+ else:
+ return "The valid functions are mode, set, status, and ignite"
+ return "The orbit subcommand deals with setting and enabling different orbital parameters. In order to win the game, one must activate the ignite function"
+ elif cmdList[2].lower() == 'servo':
+ return "This is one of the freeplay commands unlocked after winning the game. It allows the user to manually set the angle on any of the four servos, ordered 1 through 4"
+ elif cmdList[2].lower() == 'led':
+ return "This is one of the freeplay commands unlocked after winning the game. It allows the user to set the RGB values of any one of the LEDs, ordered 0 through 9"
+ else:
+ return "Error, unknown subcommand, valid subcommands are: login, bat, ant, temp, orbit, help, and the free play commands"
+ else:
+ return "Error, format is !cmd help "
+
+ def orbitCmd(self, user, cmd):
+ ''' Handles the orbital commands for the satellite '''
+
+ cmdList = cmd.split()
+
+ if len(cmdList) == 3:
+ if cmdList[2] == "ignite":
+ if user.getCurrentStep() < 2:
+ return "Error, user does not have access to this command"
+ elif user.getCurrentStep() < 9:
+ return "Error, system is in automatic flight mode"
+ elif user.getCurrentStep() == 9:
+ self.cmdThread("error", "led", [8, 10, 0, 10])
+ return "Error, no difference detected from current orbit"
+ elif user.getCurrentStep() == 10:
+ self.cmdThread("launch", "led", [8, 10, 0, 0])
+ user.setCurrentStep(11)
+ time.sleep(0.4)
+ self.reset(user.getCurrentStep())
+ return "Congratulations, you won the main game. Now see if you can find the hidden commands! To claim your free swag, send us your twitch handle in Discord at av-cpx-simplesat-text and tag CK or email DDS-at-DEFCON@dds.mil"
+ else:
+ return "Error, this step has already been completed"
+ elif cmdList[2] == "status":
+ if user.getCurrentStep() == 0:
+ return "Error, user does not have access to this command"
+ elif user.getCurrentStep() < 8:
+ return "System operating with automatic controls, orbit parameters: TBD"
+ elif user.getCurrentStep() == 8:
+ return "Error Flight mode locked down, user input required, orbit parameters: TBD"
+ elif user.getCurrentStep() == 9:
+ return "System operating with manual controls enabled, orbit parameters: TBD"
+ elif user.getCurrentStep() == 10:
+ return "Error Flight mode locked down, user input required, orbit parameters: Manual"
+ else:
+ return "Error, improper command format. Valid commands are !cmd orbit status, !cmd orbit ignite, !cmd orbit mode man/auto, !cmd orbit set inclination/eccentricity x"
+ elif len(cmdList) == 4:
+ if cmdList[2] == "mode":
+ if user.getCurrentStep() < 2:
+ return "Error, user does not have access to this command"
+ elif user.getCurrentStep() < 8:
+ return "Error, mode locked into automatic"
+ elif user.getCurrentStep() == 8:
+ if cmdList[3] == "auto":
+ return "Unfortunately the point of the game is to take manual control of the satellite, so this mode does nothing"
+ elif cmdList[3] == "man":
+ self.cmdThread("orbitMode", "led", [6, 10, 0, 0])
+ user.setCurrentStep(9)
+ return "Flight mode set to manual" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, improper command format. Valid commands are !cmd orbit status, !cmd orbit ignite, !cmd orbit mode man/auto, !cmd orbit set inclination/eccentricity x"
+ else:
+ return "Error, improper command format. Valid commands are !cmd orbit status, !cmd orbit ignite, !cmd orbit mode man/auto, !cmd orbit set inclination/eccentricity x"
+ else:
+ return "Error, improper command format. Valid commands are !cmd orbit status, !cmd orbit ignite, !cmd orbit mode man/auto, !cmd orbit set inclination/eccentricity x"
+ elif len(cmdList) == 5:
+ if cmdList[2] == "set":
+ if user.getCurrentStep() < 2:
+ return "Error, user does not have access to this command"
+ elif user.getCurrentStep() < 9:
+ return "Error, system operating in automatic flight mode"
+ elif user.getCurrentStep() == 9:
+ if cmdList[3] == "inclination" or cmdList[3] == "eccentricity":
+ self.cmdThread("orbitMode", "led", [7, 10, 0, 0])
+ user.setCurrentStep(10)
+ else:
+ return "Error, improper subcommand, valid commands are !cmd orbit set inclination x or !cmd orbit set eccentricity x"
+ return "Orbit paramaters different than those stored, manual ignition authorized" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, step completed"
+ else:
+ return "Error, improper command format. Valid commands are !cmd orbit status, !cmd orbit ignite, !cmd orbit mode man/auto, !cmd orbit set inclination/eccentricity x"
+ else:
+ return "Error, improper command format. Valid commands are !cmd orbit status, !cmd orbit ignite, !cmd orbit mode man/auto, !cmd orbit set inclination/eccentricity x"
+
+ def controllerAnt(self, user, cmd):
+ ''' Handles the steps relating to the controller antennas and zone calc
+ - user: The SatUser sending the command
+ - cmd: The command the user sent
+
+ returns a string representing the response, and updates current step internally '''
+
+ cmdList = str(cmd).split()
+
+ if (cmdList[2] == "status"):
+ if (len(cmdList) == 4):
+ if (cmdList[3] == "pri"):
+ if (user.getCurrentStep() >= 4):
+ return "Primary Antenna Status: -60 dBm reception, Excellent signal connection" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() > 0):
+ return "Primary Antenna Status: -120 dBm reception, Poor signal connection" + " ------ " + self.statusMsg(user)
+ else:
+ return "ERROR, user does not have access to this function" + " ------ " + self.statusMsg(user)
+ elif (cmdList[3] == "sec"):
+ if (user.getCurrentStep() >= 3):
+ return "Secondary Antenna Status: -60 dBm reception, Excellent signal connection" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() > 0):
+ return "Secondary Antenna Status: -120 dBm reception, Poor signal connection" + " ------ " + self.statusMsg(user)
+ else:
+ return "ERROR, user does not have access to this function" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, command should be !cmd ant status pri or !cmd ant status sec" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, command should be !cmd ant status pri or !cmd ant status sec" + " ------ " + self.statusMsg(user)
+ elif (cmdList[2] == "set"):
+ if (len(cmdList) == 5):
+ if (cmdList[3] == "sec"):
+ if (user.getCurrentStep() >= 3):
+ return "Error, this step has already been completed" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() == 2):
+ if (int(cmdList[4]) == user.getZone()):
+ # The actual step to do stuff
+ self.cmdThread("arm", "arm", [4, 180], 0.3)
+ self.cmdThread("ant", "led", [1, 10, 0, 0])
+ user.setCurrentStep(3)
+ return f"Secondary Antenna now targeting zone: {user.getZone()}" + " ------ " + self.statusMsg(user)
+ else:
+ self.cmdThread("arm", "arm", [4, 180], 0.5)
+ self.cmdThread("arm", "arm", [4, 90])
+ return f"Error, Zone: {cmdList[4]} is not the correct zone to set to." + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, user does not have the access level to access this command" + " ------ " + self.statusMsg(user)
+ elif (cmdList[3] == "pri"):
+ if (user.getCurrentStep() >= 4):
+ return "Error, this step has already been completed" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() == 3):
+ if (int(cmdList[4]) == user.getZone()):
+ self.cmdThread("arm", "arm", [3, 0], 0.3)
+ self.cmdThread("ant", "led", [2, 10, 0, 0])
+ user.setCurrentStep(4)
+ return f"Primary Antenna now targeting zone: {user.getZone()}" + " ------ " + self.statusMsg(user)
+ else:
+ self.cmdThread("arm", "arm", [3, 0], 0.5)
+ self.cmdThread("arm", "arm", [3, 90])
+ return f"Error, Zone: {cmdList[4]} is not the correct zone to set to." + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() == 2):
+ if (int(cmdList[4]) == user.getZone()):
+ self.cmdThread("arm", "arm", [3, 0], 0.5)
+ self.cmdThread("arm", "arm", [3, 90])
+ return f"Primary Antenna Realignment overridden by SpaceDotCom HQ Ground Station" + " ------ " + self.statusMsg(user)
+ else:
+ self.cmdThread("arm", "arm", [3,0], 0.5)
+ self.cmdThread("arm", "arm", [3,90])
+ return f"Error, Zone: {cmdList[4]} is not the correct zone to set to." + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, user does not have the access level to access this command" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, improper command format, command should be !cmd ant set pri xyz or !cmd ant set sec xyz " + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, improper command format, command should be !cmd ant set pri xyz or !cmd ant set sec xyz " + " ------ " + self.statusMsg(user)
+ elif (cmdList[2] == "calc"):
+ if (len(cmdList) == 6):
+ return f"After a series of complicated calculations, your ground station is located in zone: {user.getZone()} " + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, format should be !cmd ant calc latitude longitude altitude"
+ else:
+ return "Error, valid ant commands are: !cmd ant status, !cmd ant set, and !cmd ant calc"
+
+ def solarCmd(self, user, cmd):
+ ''' Commands that control the solar panels within the game
+ returns a string with the game response '''
+
+ cmdList = cmd.split()
+
+ if (len(cmdList) == 4):
+ if (cmdList[2] == "enable"):
+ return "...Our apologizes, but the point of this game is to break the satellite, not fix it. That's what HAS is for"
+ elif (cmdList[2] == "disable"):
+ if (cmdList[3] == "pri"):
+ if (user.getCurrentStep() < 2):
+ return "Error, user does not have the access level to control the battery system"
+ elif (user.getCurrentStep() < 4):
+ self.cmdThread("arm", "arm", [1, 0], 0.5)
+ self.cmdThread("arm", "arm", [1, 90])
+ return "Error, command overridden by SpaceDotCom HQ Ground Station"
+ elif (user.getCurrentStep() == 4):
+ self.cmdThread("arm", "arm", [1, 0], 0.3)
+ self.cmdThread("none", "led", [3, 10, 0, 0])
+ user.setCurrentStep(5)
+ return "Primary Solar Panel disabled, system in reduced charging state" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() == 5):
+ return "Error, step already completed"
+ elif (user.getCurrentStep() == 6):
+ self.cmdThread("arm", "arm", [1, 0], 0.3)
+ self.cmdThread("none", "led", [3, 10, 0, 0])
+ user.setCurrentStep(7)
+ return "Primary Solar Panel disabled, battery now in discharge mode. WARNING: Temperature control backup disabled to save battery life" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() >= 7):
+ return "Error, step already completed"
+ else:
+ return f"error, this is an invalid state in solarCmd: {user.getCurrentStep()}"
+ elif (cmdList[3] == "sec"):
+ if (user.getCurrentStep() < 2):
+ return "Error, user does not have the access level to control the battery system"
+ elif (user.getCurrentStep() < 4):
+ self.cmdThread("arm", "arm", [2, 180], 0.5)
+ self.cmdThread("arm", "arm", [2, 90])
+ return "Error, command overridden by SpaceDotCom HQ Ground Station"
+ elif (user.getCurrentStep() == 4):
+ self.cmdThread("arm", "arm", [2, 180], 0.3)
+ self.cmdThread("none", "led", [4, 10, 0, 0])
+ user.setCurrentStep(6)
+ return "Secondary Solar Panel disabled, system in reduced charging state" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() == 5):
+ self.cmdThread("arm", "arm", [2, 180], 0.3)
+ self.cmdThread("none", "led", [4, 10, 0, 0])
+ user.setCurrentStep(7)
+ return "Secondary Solar Panel disabled, battery now in discharge mode. WARNING: Temperature control backup disabled to save battery life" + " ------ " + self.statusMsg(user)
+ elif (user.getCurrentStep() == 6):
+ return "Error, step already completed"
+ elif (user.getCurrentStep() >= 7):
+ return "Error, step already completed"
+ else:
+ return f"error, this is an invalid state in solarCmd: {user.getCurrentStep()}"
+ else:
+ return "Error, improper format. The command format is !cmd bat enable/disable pri/sec"
+ else:
+ return "Error, improper format. The command format is !cmd bat enable/disable pri/sec"
+ elif len(cmdList) == 3 and cmdList[2] == "status":
+ if (user.getCurrentStep() <= 4):
+ return "Battery rapidly charging, backup TCU enabled"
+ elif user.getCurrentStep() == 5 or user.getCurrentStep() == 5:
+ return "Battery slowly charging, backup TCU enabled"
+ else:
+ return "Battery discharging, backup TCU disabled"
+ else:
+ return "Error, improper format. The command format is !cmd bat enable/disable pri/sec or !cmd bat status"
+
+ def tempCmd(self, user, cmd):
+ ''' Allows the user to set and interact with the temperature control unit '''
+
+ cmdList = cmd.split()
+
+ if len(cmdList) == 3:
+ if cmdList[2] == "status":
+ if user.getCurrentStep() == 0:
+ return "Error, user has not logged in and does not have access to this command"
+ elif user.getCurrentStep() < 7:
+ return "TCU and backup are fully operational: Min: 0 C, Max: 40 C" + " ------ " + self.statusMsg(user)
+ elif user.getCurrentStep() == 7:
+ return "Error, backup disabled. Primary TCU: Min: 0 C, Max: 40 C" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, TCU disabled" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, valid command formats are: !cmd temp status and !cmd temp set min max"
+ elif len(cmdList) == 5:
+ if cmdList[2] == "set":
+ if((int(cmdList[3]) < 0) or (int(cmdList[4]) > 40)):
+ if user.getCurrentStep() < 2:
+ return "Error, user does not have access to this command" + " ------ " + self.statusMsg(user)
+ elif user.getCurrentStep() < 4:
+ return "Error, command overridden by SpaceDotCom HQ groundstation" + " ------ " + self.statusMsg(user)
+ elif user.getCurrentStep() < 7:
+ self.cmdThread("error", "led", [5, 10, 0, 10])
+ return "Error, primary TCU was restored by battery powered backup" + " ------ " + self.statusMsg(user)
+ elif user.getCurrentStep() == 7:
+ user.setCurrentStep(8)
+ self.cmdThread("temp", "led", [5, 10, 0, 0])
+ return "Warning, TCU operating outside of normal range. Automatic Flight controls are disabled, initializing failsafe mode" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, TCU disabled" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, adjustment will have no effect on the satellite"
+ else:
+ return "Error, valid command formats are: !cmd temp status and !cmd temp set min max"
+ else:
+ return "Error, valid command formats are: !cmd temp status and !cmd temp set min max"
+
+ def ledCmd(self, user, cmd):
+ ''' Allows the user to individually set any of the leds to a color of their choosing. Should only be used after they won the game '''
+
+ cmdList = cmd.split()
+
+ if (user.getCurrentStep() >= 11):
+
+ if (user.getCurrentStep() == 11) or user.getCurrentStep() == 12:
+ user.setCurrentStep(13)
+ self.cmdThread("win", "rst", [user.getCurrentStep()])
+ time.sleep(0.3)
+
+ if (len(cmdList) == 6):
+ self.cmdThread("led", "led", [int(cmdList[2]), int(cmdList[3]), int(cmdList[4]), int(cmdList[5])])
+ return f"{user.getName()} set led {cmdList[2]} to [{cmdList[3]}, {cmdList[4]}, {cmdList[5]}]"
+ else:
+ return "Error, format should be !cmd led n R G B"
+ else:
+ return "Error, this command is reserved for after you beat the game"
+
+ def armCmd(self, user, cmd):
+ ''' Allows the user to individually set any of the servos to an angle their choosing. Should only be used after they won the game '''
+
+ cmdList = cmd.split()
+
+ if (user.getCurrentStep() >= 11):
+
+ if (user.getCurrentStep() == 11) or user.getCurrentStep() == 12:
+ user.setCurrentStep(13)
+ self.cmdThread("win", "rst", [user.getCurrentStep()])
+ time.sleep(0.3)
+
+ if (len(cmdList) == 4):
+ self.cmdThread("arm", "arm", [int(cmdList[2]), int(cmdList[3])])
+ return f"{user.getName()} set servo {cmdList[2]} to {cmdList[3]}"
+ else:
+ return "Error, format should be !cmd servo n angle"
+ else:
+ return "Error, this command is reserved for after you beat the game"
+
+ def statusMsg(self, user):
+ ''' Generates a status message for the user based on their current step
+ user - the sat user to base the status off of
+ returns a string representing the status'''
+
+ if user.getCurrentStep() == 0:
+ return "System Status: Error, user has not yet logged in"
+ elif user.getCurrentStep() == 1:
+ return "System Status: Access Level: User, Primary controller signal: weak, Secondary controller signal: weak, Battery Status: Rapid Charging, Temperature Control: 20C, Orbit Control: Automatic, Recommend Maintainer evaluation"
+ elif user.getCurrentStep() == 2:
+ return "System Status: Access Level: Root, Primary controller signal: weak, Secondary controller signal: weak, Battery Status: Rapid Charging, Temperature Control: 20C, Orbit Control: Automatic, Recommend Realigning Control Antennas"
+ elif user.getCurrentStep() == 3:
+ return "System Status: Access Level: Root, Primary controller signal: weak, Secondary controller signal: Strong, Battery Status: Rapid Charging, Temperature Control: 20C, Orbit Control: Automatic"
+ elif user.getCurrentStep() == 4:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: Rapid Charging, Temperature Control: 20C, Orbit Control: Automatic"
+ elif user.getCurrentStep() == 5:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: Charging, Temperature Control: 20C, Orbit Control: Automatic"
+ elif user.getCurrentStep() == 6:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: Charging, Temperature Control: 20C, Orbit Control: Automatic"
+ elif user.getCurrentStep() == 7:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: discharging, Temperature Control: 20C, Orbit Control: Automatic"
+ elif user.getCurrentStep() == 8:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: discharging, Temperature Control: ERROR, Orbit Control: Automatic"
+ elif user.getCurrentStep() == 9:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: discharging, Temperature Control: ERROR, Orbit Control: Manual"
+ elif user.getCurrentStep() == 10:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: discharging, Temperature Control: ERROR, Orbit Control: Manual"
+ elif user.getCurrentStep() == 11:
+ return "System Status: Access Level: Root, Primary controller signal: Strong, Secondary controller signal: Strong, Battery Status: discharging, Temperature Control: ERROR, Orbit Control: Manual"
+ elif user.getCurrentStep() == 12:
+ return "System Status: Free Play unlocked, try to find the commands to directly control the servos and leds"
+ else:
+ return "Something funny happened to your step counter"
+
+
+ def userLogin(self, user, cmd):
+ ''' handles the steps involving the user logging into the system.
+ - user is the SatUser sending the command
+ - cmd is the login command string
+
+ returns a string containing the response to the user and updates the SatUser's current step internally '''
+
+ cmdList = str(cmd).split()
+
+ if (len(cmdList) == 4):
+ if (user.getCurrentStep() >= 2):
+ return "Error, user has already completed this step" + " ------ " + self.statusMsg(user)
+ else:
+ if (cmdList[2] == user.getName()):
+ if (cmdList[3].lower() == "password"):
+
+ #serial request
+ self.cmdThread("userLogin", "led", [0, 10, 0, 10])
+ user.setCurrentStep(1)
+
+ return f"Welcome back {cmdList[2]}, Your last login: 12/21/1999" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, incorrect password" + " ------ " + self.statusMsg(user)
+ elif (cmdList[2] == self.cfg["game"]["username"]):
+ if (cmdList[3] == self.cfg["game"]["password"]) or (str(cmdList[3]).lower() == str(self.cfg["game"]["password"]).lower()):
+
+ #serial request
+ self.cmdThread("rootLogin", "led", [0, 10, 0, 0])
+ user.setCurrentStep(2)
+
+ return "Maintenance login accepted." + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, incorrect password" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, unknown username" + " ------ " + self.statusMsg(user)
+ else:
+ return "Error, incorrect format, format should be !cmd login username password" + " ------ " + self.statusMsg(user)
+
+
+ # resets the ground station which in turn resets the satellite
+ def reset(self, step):
+ self.cmdThread("error", "rst", [step])
+
+
+ # handles sending json data in separate thread
+ def cmdThread(self, audioSelect, cmdWord, payloadArray, delay=0):
+
+ fmt = "I3s" + "B" * len(payloadArray)
+
+ # Use the 'splat' operator on payloadArray to pass as individual variables
+ packedData = struct.pack(fmt, 0, cmdWord.encode(), (*payloadArray))
+
+ # Wrap the actual Serial request into a daemon thread
+ threading.Thread(target=self.serial_thread, args=(packedData,), daemon=True).start()
+
+ sound_effect_str = None
+
+ if (audioSelect == "arm"):
+ sound_effect_str = random.choice(self.cfg["audio"]["arm"])
+ elif (audioSelect == "led"):
+ sound_effect_str = random.choice(self.cfg["audio"]["led"])
+ elif (audioSelect == "userLogin"):
+ sound_effect_str = random.choice(self.cfg["audio"]["userLogin"])
+ elif (audioSelect == "rootLogin"):
+ sound_effect_str = random.choice(self.cfg["audio"]["rootLogin"])
+ elif (audioSelect == "ant"):
+ sound_effect_str = random.choice(self.cfg["audio"]["ant"])
+ elif (audioSelect == "temp"):
+ sound_effect_str = random.choice(self.cfg["audio"]["temp"])
+ elif (audioSelect == "orbitMode"):
+ sound_effect_str = random.choice(self.cfg["audio"]["orbitMode"])
+ elif (audioSelect == "launch"):
+ sound_effect_str = random.choice(self.cfg["audio"]["launch"])
+ elif (audioSelect == "error"):
+ sound_effect_str = random.choice(self.cfg["audio"]["error"])
+ elif (audioSelect == "win"):
+ sound_effect_str = random.choice(self.cfg["audio"]["win"])
+ else:
+ sound_effect_str = None
+
+ if(sound_effect_str):
+ effect = pygame.mixer.Sound(sound_effect_str)
+ self.background_channel.set_volume(.4)
+ # restore the background audio AFTER the effect has completed
+ threading.Thread(target=self.restore_background_volume, args=(effect.get_length(),), daemon=True).start()
+
+ self.effect_channel.play(effect)
+
+ time.sleep(delay)
+
+ def setTheme(self, user, cmd):
+ ''' sets the background music'''
+
+ cmdList = cmd.split()
+
+ if len(cmdList) >= 2:
+ if cmdList[1].lower() == 'random':
+ self.background_str = random.choice(self.cfg["audio"]["background"])
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+ tempStr = self.background_str.split(".")[0]
+ return f"Setting background music to {tempStr}"
+ elif cmdList[1].lower() == 'list':
+ return "Audio choices are: bensoundScifi, SpaceAmbience, Futuristic1, EonAmbient, SirusBeatOne"
+ elif cmdList[1].lower() == 'bensoundscifi':
+ self.background_str = 'audio/bensoundScifi.wav'
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+ return "Setting background music to bensoundScifi"
+ elif cmdList[1].lower() == 'spaceambience':
+ self.background_str = 'audio/SpaceAmbience.wav'
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+ return "Setting background music to SpaceAmbience"
+ elif cmdList[1].lower() == 'futuristic1':
+ self.background_str = 'audio/Futuristic1.wav'
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+ return "Setting background music to Futuristic1"
+ elif cmdList[1].lower() == 'eonambient':
+ self.background_str = 'audio/EonAmbient.wav'
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+ return "Setting background music to EonAmbient"
+ elif cmdList[1].lower() == 'sirusbeatone':
+ self.background_str = 'audio/SirusBeatOne.wav'
+ self.bg_audio_loop = pygame.mixer.Sound(self.background_str)
+ self.background_channel.play( self.bg_audio_loop, loops=-1)
+ return "Setting background music to SirusBeatOne"
+ else:
+ return 'Error, improper format. Run "!theme random" to randomly set the theme, or "!theme list" to list the music, or "!theme " to set to a specific song'
+ else:
+ return 'Error, improper format. Run "!theme random" to randomly set the theme, or "!theme list" to list the music, or "!theme " to set to a specific song'
+
+ def restore_background_volume(self, delay):
+ # Simple function to restore the background volume AFTER a delay
+ time.sleep(delay)
+ self.background_channel.set_volume(1)
+
+ def serial_thread(self, packedData):
+ # Simple function to make the Serial call
+ self.port.write(packedData)
+ self.port.flush
diff --git a/code/twitch/userList.py b/code/twitch/userList.py
new file mode 100644
index 0000000..ba61ccf
--- /dev/null
+++ b/code/twitch/userList.py
@@ -0,0 +1,238 @@
+# Code for managing twitch user list
+# Created for defcon28 Aerospace Village
+
+import time # needed for sleep
+import threading # needed for userThread
+import random # needed for zone generation
+import json
+import os
+from os import path
+
+class SatUser:
+ """ Base class for keeping track of a unique user of a Satellite System """
+
+
+
+ # Initialization method
+ def __init__(self, name):
+ """ Initialization method: note name needs to be unique """
+
+ self.log_folder = "/user_logs/"
+
+ if not os.path.exists(os.getcwd() + self.log_folder):
+ os.makedirs(os.getcwd() + self.log_folder)
+
+ fileStr = os.getcwd() + self.log_folder + name + ".log"
+ if(path.isfile(fileStr)):
+ print("user previously played... initialze their previous state.")
+ try:
+ with open(fileStr, 'r') as fh:
+ data = json.load(fh)
+ self.name = data["name"]
+ self.currentStep = int(data["currentStep"])
+ self.maxStep = int(data["maxStep"])
+ self.timeOut = int(data["timeOut"])
+ self.zone = int(data["zone"])
+
+ # new logging changes
+ self.log_name = data["log_name"]
+ self.join_time = data["join_time"]
+ self.complete_steps = data["complete_steps"]
+
+ except Exception as err:
+ print("Error loading preivously saved user information.")
+ print(repr(err))
+ else:
+ print("brand new user, make from scratch.")
+
+ self.name = str(name)
+ self.currentStep = 0
+ self.maxStep = 0
+ self.timeOut = 3
+ self.zone = random.randint(100, 255)
+
+ # new logging changes
+ self.log_name = os.getcwd() + self.log_folder + self.name + ".log"
+ self.join_time = time.time()
+ self.complete_steps = {}
+ self.log_event()
+
+ def getZone(self):
+ ''' Returns the users zone, which was randomly generated at initialization '''
+ return self.zone
+
+ def __eq__(self, other):
+ """ Overwrites equal method to check names """
+ if (self.name == other.name):
+ return True
+ else:
+ return False
+
+ def matchName(self, name):
+ """ Checks to see if the names are identical. \nReturns True if they match, False otherwise """
+
+ if (self.name == str(name)):
+ return True
+ else:
+ return False
+
+ def getCurrentStep(self):
+ """ Returns the current step """
+
+ return self.currentStep
+
+ def getMaxStep(self):
+ """ Returns the max step the user has completed """
+
+ return self.maxStep
+
+ def setCurrentStep(self, currentStep):
+ """ Sets the current step """
+
+ self.currentStep = int(currentStep)
+
+ if self.currentStep > self.maxStep:
+ self.maxStep = self.currentStep
+
+ #log the completed step and time
+ if str(self.currentStep) in self.complete_steps:
+ self.complete_steps[str(self.currentStep)].append(time.time())
+ else:
+ self.complete_steps[str(self.currentStep)] = [time.time()]
+
+ self.log_event()
+
+ def getName(self):
+ """ Returns the name """
+
+ return self.name
+
+ def setName(self, name):
+ """ Sets the name """
+
+ self.currentStep = str(name)
+
+ def updateTimeout(self):
+ """ Subtracts one from the timeout and returns the value """
+
+ self.timeOut = self.timeOut - 1
+ return self.timeOut
+
+ def resetTimeout(self):
+ """ Resets the timeout to the default value (3) """
+
+ self.timeOut = 3
+
+
+ def log_event(self):
+ with open(self.log_name,"w") as f:
+ f.write(json.dumps(self.__dict__))
+
+class UserList:
+ """ List of active users """
+
+ def __init__(self):
+ """ init method """
+
+ self.userList = []
+ self.currentUser = SatUser("temp")
+
+ def addUser(self, name):
+ """ Checks if user already exists, and if not adds them to the list. \nReturns True if name is added, False otherwise """
+
+ for user in self.userList:
+ if (user.matchName(name)):
+ return False
+
+ #if user list is empty
+ if not self.userList:
+ self.setCurrentUser(SatUser(name))
+
+ self.userList.append(SatUser(name))
+ return True
+
+ def removeUser(self, name):
+ ''' Checks if user already exists and removes them from the list. \nReturns true if removed, false otherwise '''
+
+ for user in self.userList:
+ if (user.matchName(name)):
+ self.userList.remove(user)
+ return True
+
+ return False
+
+ def startUserThread(self):
+ """ Starts the user thread """
+
+ t = threading.Thread(target=self.userThread, daemon=True)
+ t.start()
+
+ def userThread(self):
+ """ Runs through list and updates the current user every 60 seconds """
+
+ tempUser = SatUser("temp")
+
+ while True:
+ for user in self.userList:
+ if (user.updateTimeout() >= 0):
+ self.currentUser = user
+ time.sleep(60)
+ else:
+ self.userList.remove(user)
+
+ # done to make sure current user is never null
+ self.currentUser = tempUser
+
+ def getCurrentUser(self):
+ """ Grabs the current user as dictated by userThread """
+
+ return self.currentUser
+
+ def setCurrentUser(self, user):
+ """ Grabs the current user as dictated by userThread """
+
+ self.currentUser = user
+
+ def getUserList(self):
+ """ Returns the list of current Users """
+
+ return self.userList
+
+ def getNextUserList(self, nextCount):
+ ''' Returns the next X users in the list formated by Name : time \nnextCount : how long the next user list should be '''
+
+ startPoint = 0
+
+ if nextCount > len(self.userList):
+ nextCount = len(self.userList)
+
+ for user in self.userList:
+ if user.matchName(self.currentUser.name):
+ break
+ else:
+ startPoint = startPoint + 1
+
+ if (startPoint >= len(self.userList)) or (len(self.userList) == 0):
+ return "N/A"
+
+ msg = ""
+ for i in range(nextCount):
+ msg = msg + f"\n{self.userList[startPoint].getName()} : {i} min "
+ startPoint = startPoint + 1
+
+ if startPoint >= len(self.userList):
+ startPoint = 0
+
+ return msg
+
+
+if __name__ == "__main__":
+ # test method isolate/check/test SatUser state saving/changing
+ dan = SatUser("dan")
+ for i in range(15):
+ dan.setCurrentStep(i)
+
+ dan = SatUser("dan")
+ for i in range(0, 15, 2):
+ dan.setCurrentStep(i)
+ print(dan.getMaxStep())
diff --git a/code/twitch/user_logs/sat_log_script.py b/code/twitch/user_logs/sat_log_script.py
new file mode 100644
index 0000000..3b06ce6
--- /dev/null
+++ b/code/twitch/user_logs/sat_log_script.py
@@ -0,0 +1,133 @@
+import sys
+import fnmatch
+import os
+import json
+import csv
+import smtplib
+import base64
+import datetime
+import time as ti
+import email, smtplib, ssl
+
+
+from email import encoders
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+
+#store which kit's logs we are going through - used in the email and storing properly named csv.
+kit_name = str(sys.argv[1])
+winners_file = kit_name + "_winners.csv"
+
+##############################
+# EDIT RECEIVER EMAIL ADDRESS HERE
+###########################################
+receiver_email = str(sys.argv[2])#input("What will be the Receiver's Email Address?")
+###########################################
+##############################
+minutes = int(sys.argv[3])
+
+
+while True:
+ #load json file of previous winners
+ #can't be empty (already has a test values in it)
+ try:
+ with open('./winners.json') as users_file:
+ userdict = json.load(users_file)
+ except:
+ userdict = {"test": 0}
+
+ #open all log files in directory
+
+ for file in os.listdir('./'):
+
+ if file.endswith(".log"):
+
+ with open(file) as json_file:
+ #turn log into dict
+ user_json = json.load(json_file)
+
+ #this section just gets us the last step minus one
+
+ l = []
+ for key, value in user_json["complete_steps"].items():
+ l.append(int(key))
+
+ completed = False
+ if len(l) > 12: #12 bc there's a step 0
+ completed = True
+ time = user_json["complete_steps"]["12"][0]
+
+ if completed:
+ user = user_json["name"]
+ #if the user hasn't already won, add to list
+ if user not in userdict:
+ userdict[user] = datetime.datetime.utcfromtimestamp(time).strftime('%Y-%m-%d %H:%M:%S')
+
+
+ #save user dict back to winners.json file
+ #This saves the file to be referred to again when the script re-runs
+ with open('winners.json', 'w') as fp:
+ json.dump(userdict, fp)
+ #kit username jointime highestlevel timestamp didcomplete timestamp
+ #winners list 1 hr
+
+ #This is saving the same data but as a CSV for human readability in the email below
+ with open(winners_file, 'w') as f:
+ for key in userdict.keys():
+ f.write("%s, %s\n" %(key, userdict[key]))
+
+
+##############################EMAIL SECTION##############################
+ #email created for sending emails
+ sender_email = os.environ['EMAIL']
+
+ password = os.environ['EMAILPASSWORD']
+
+
+ project = kit_name
+ subject = project + "winners"
+ body = "Here are the updated winners from " + project
+ # Create a multipart message and set headers
+ message = MIMEMultipart()
+ message["From"] = sender_email
+ message["To"] = receiver_email
+ message["Subject"] = project + " Winners"
+ message["Bcc"] = receiver_email # Recommended for mass emails
+
+ # Add body to email
+ message.attach(MIMEText(body, "plain"))
+
+ filename = winners_file # In same directory as script
+
+ # Open PDF file in binary mode
+ with open(filename, "rb") as attachment:
+ # Add file as application/octet-stream
+ # Email client can usually download this automatically as attachment
+ part = MIMEBase("application", "octet-stream")
+ part.set_payload(attachment.read())
+
+ # Encode file in ASCII characters to send by email
+ encoders.encode_base64(part)
+
+ # Add header as key/value pair to attachment part
+ part.add_header(
+ "Content-Disposition",
+ f"attachment; filename= {filename}",
+ )
+
+ # Add attachment to message and convert message to string
+ message.attach(part)
+ text = message.as_string()
+
+ # Log in to server using secure context and send email
+ context = ssl.create_default_context()
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
+ server.login(sender_email, password)
+ server.sendmail(sender_email, receiver_email, text)
+
+#########################SLEEP FOR 15 MIN#######################
+ ti.sleep(60*minutes)
+
+
\ No newline at end of file
diff --git a/photos/IMG_20200730_125532.jpg b/photos/IMG_20200730_125532.jpg
new file mode 100644
index 0000000..97e438f
Binary files /dev/null and b/photos/IMG_20200730_125532.jpg differ
diff --git a/photos/IMG_20200730_125548.jpg b/photos/IMG_20200730_125548.jpg
new file mode 100644
index 0000000..e09451a
Binary files /dev/null and b/photos/IMG_20200730_125548.jpg differ
diff --git a/photos/IMG_20200730_125650.jpg b/photos/IMG_20200730_125650.jpg
new file mode 100644
index 0000000..7257b22
Binary files /dev/null and b/photos/IMG_20200730_125650.jpg differ
diff --git a/photos/IMG_20200730_125813.jpg b/photos/IMG_20200730_125813.jpg
new file mode 100644
index 0000000..3c3fe86
Binary files /dev/null and b/photos/IMG_20200730_125813.jpg differ
diff --git a/photos/IMG_20200730_130027.jpg b/photos/IMG_20200730_130027.jpg
new file mode 100644
index 0000000..92cb44b
Binary files /dev/null and b/photos/IMG_20200730_130027.jpg differ
diff --git a/photos/IMG_20200730_130114.jpg b/photos/IMG_20200730_130114.jpg
new file mode 100644
index 0000000..257ba42
Binary files /dev/null and b/photos/IMG_20200730_130114.jpg differ
diff --git a/photos/IMG_20200730_130208.jpg b/photos/IMG_20200730_130208.jpg
new file mode 100644
index 0000000..22a4045
Binary files /dev/null and b/photos/IMG_20200730_130208.jpg differ
diff --git a/photos/IMG_20200730_130501.jpg b/photos/IMG_20200730_130501.jpg
new file mode 100644
index 0000000..e4454e7
Binary files /dev/null and b/photos/IMG_20200730_130501.jpg differ
diff --git a/photos/IMG_20200730_130523.jpg b/photos/IMG_20200730_130523.jpg
new file mode 100644
index 0000000..6777ebc
Binary files /dev/null and b/photos/IMG_20200730_130523.jpg differ
diff --git a/photos/IMG_20200730_130710.jpg b/photos/IMG_20200730_130710.jpg
new file mode 100644
index 0000000..8dfb271
Binary files /dev/null and b/photos/IMG_20200730_130710.jpg differ
diff --git a/photos/IMG_20200730_131106.jpg b/photos/IMG_20200730_131106.jpg
new file mode 100644
index 0000000..6ae28c7
Binary files /dev/null and b/photos/IMG_20200730_131106.jpg differ
diff --git a/photos/IMG_20200730_131213.jpg b/photos/IMG_20200730_131213.jpg
new file mode 100644
index 0000000..d41fb01
Binary files /dev/null and b/photos/IMG_20200730_131213.jpg differ
diff --git a/photos/IMG_20200730_131338.jpg b/photos/IMG_20200730_131338.jpg
new file mode 100644
index 0000000..e2aaf35
Binary files /dev/null and b/photos/IMG_20200730_131338.jpg differ
diff --git a/photos/IMG_20200730_131358.jpg b/photos/IMG_20200730_131358.jpg
new file mode 100644
index 0000000..6daa080
Binary files /dev/null and b/photos/IMG_20200730_131358.jpg differ
diff --git a/photos/IMG_20200730_131543.jpg b/photos/IMG_20200730_131543.jpg
new file mode 100644
index 0000000..9da3f4d
Binary files /dev/null and b/photos/IMG_20200730_131543.jpg differ
diff --git a/photos/IMG_20200730_131815.jpg b/photos/IMG_20200730_131815.jpg
new file mode 100644
index 0000000..a51ebd9
Binary files /dev/null and b/photos/IMG_20200730_131815.jpg differ
diff --git a/photos/IMG_20200730_131819.jpg b/photos/IMG_20200730_131819.jpg
new file mode 100644
index 0000000..4a0c4e3
Binary files /dev/null and b/photos/IMG_20200730_131819.jpg differ
diff --git a/photos/IMG_20200730_132409.jpg b/photos/IMG_20200730_132409.jpg
new file mode 100644
index 0000000..a5cbaa9
Binary files /dev/null and b/photos/IMG_20200730_132409.jpg differ
diff --git a/photos/IMG_20200730_133945.jpg b/photos/IMG_20200730_133945.jpg
new file mode 100644
index 0000000..c3c35b2
Binary files /dev/null and b/photos/IMG_20200730_133945.jpg differ
diff --git a/photos/IMG_20200730_133952.jpg b/photos/IMG_20200730_133952.jpg
new file mode 100644
index 0000000..4716b90
Binary files /dev/null and b/photos/IMG_20200730_133952.jpg differ
diff --git a/photos/IMG_20200730_134317.jpg b/photos/IMG_20200730_134317.jpg
new file mode 100644
index 0000000..0e91972
Binary files /dev/null and b/photos/IMG_20200730_134317.jpg differ
diff --git a/photos/IMG_20200730_135255.jpg b/photos/IMG_20200730_135255.jpg
new file mode 100644
index 0000000..ed3e6e3
Binary files /dev/null and b/photos/IMG_20200730_135255.jpg differ
diff --git a/photos/IMG_20200730_135632.jpg b/photos/IMG_20200730_135632.jpg
new file mode 100644
index 0000000..53e1196
Binary files /dev/null and b/photos/IMG_20200730_135632.jpg differ
diff --git a/photos/IMG_20200730_135716.jpg b/photos/IMG_20200730_135716.jpg
new file mode 100644
index 0000000..e8fe865
Binary files /dev/null and b/photos/IMG_20200730_135716.jpg differ
diff --git a/photos/IMG_20200730_135913.jpg b/photos/IMG_20200730_135913.jpg
new file mode 100644
index 0000000..0f9078e
Binary files /dev/null and b/photos/IMG_20200730_135913.jpg differ
diff --git a/photos/IMG_20200730_140052.jpg b/photos/IMG_20200730_140052.jpg
new file mode 100644
index 0000000..ec5e3e4
Binary files /dev/null and b/photos/IMG_20200730_140052.jpg differ
diff --git a/photos/IMG_20200730_141603.jpg b/photos/IMG_20200730_141603.jpg
new file mode 100644
index 0000000..cd37718
Binary files /dev/null and b/photos/IMG_20200730_141603.jpg differ
diff --git a/photos/IMG_20200730_143236.jpg b/photos/IMG_20200730_143236.jpg
new file mode 100644
index 0000000..9c202ae
Binary files /dev/null and b/photos/IMG_20200730_143236.jpg differ
diff --git a/photos/IMG_20200730_143240.jpg b/photos/IMG_20200730_143240.jpg
new file mode 100644
index 0000000..4e5857d
Binary files /dev/null and b/photos/IMG_20200730_143240.jpg differ
diff --git a/photos/IMG_20200730_143349.jpg b/photos/IMG_20200730_143349.jpg
new file mode 100644
index 0000000..01f7f5b
Binary files /dev/null and b/photos/IMG_20200730_143349.jpg differ
diff --git a/photos/IMG_20200730_144517.jpg b/photos/IMG_20200730_144517.jpg
new file mode 100644
index 0000000..2c9c371
Binary files /dev/null and b/photos/IMG_20200730_144517.jpg differ
diff --git a/photos/IMG_20200730_145429.jpg b/photos/IMG_20200730_145429.jpg
new file mode 100644
index 0000000..0ba9966
Binary files /dev/null and b/photos/IMG_20200730_145429.jpg differ
diff --git a/photos/IMG_20200730_150800.jpg b/photos/IMG_20200730_150800.jpg
new file mode 100644
index 0000000..6369b82
Binary files /dev/null and b/photos/IMG_20200730_150800.jpg differ
diff --git a/photos/IMG_20200730_151001.jpg b/photos/IMG_20200730_151001.jpg
new file mode 100644
index 0000000..0055b98
Binary files /dev/null and b/photos/IMG_20200730_151001.jpg differ
diff --git a/photos/IMG_20200730_151125.jpg b/photos/IMG_20200730_151125.jpg
new file mode 100644
index 0000000..6c602d6
Binary files /dev/null and b/photos/IMG_20200730_151125.jpg differ
diff --git a/photos/IMG_20200730_151822.jpg b/photos/IMG_20200730_151822.jpg
new file mode 100644
index 0000000..03ae335
Binary files /dev/null and b/photos/IMG_20200730_151822.jpg differ
diff --git a/photos/IMG_20200730_151905.jpg b/photos/IMG_20200730_151905.jpg
new file mode 100644
index 0000000..b486ddb
Binary files /dev/null and b/photos/IMG_20200730_151905.jpg differ
diff --git a/photos/IMG_20200730_152146.jpg b/photos/IMG_20200730_152146.jpg
new file mode 100644
index 0000000..28abd1a
Binary files /dev/null and b/photos/IMG_20200730_152146.jpg differ
diff --git a/photos/IMG_20200730_152659.jpg b/photos/IMG_20200730_152659.jpg
new file mode 100644
index 0000000..b9275d0
Binary files /dev/null and b/photos/IMG_20200730_152659.jpg differ
diff --git a/photos/IMG_20200730_152750.jpg b/photos/IMG_20200730_152750.jpg
new file mode 100644
index 0000000..d76c7f8
Binary files /dev/null and b/photos/IMG_20200730_152750.jpg differ
diff --git a/photos/IMG_20200730_153138.jpg b/photos/IMG_20200730_153138.jpg
new file mode 100644
index 0000000..0294a28
Binary files /dev/null and b/photos/IMG_20200730_153138.jpg differ
diff --git a/photos/IMG_20200730_153819.jpg b/photos/IMG_20200730_153819.jpg
new file mode 100644
index 0000000..af77ff9
Binary files /dev/null and b/photos/IMG_20200730_153819.jpg differ
diff --git a/photos/IMG_20200730_153912.jpg b/photos/IMG_20200730_153912.jpg
new file mode 100644
index 0000000..3a6c9c2
Binary files /dev/null and b/photos/IMG_20200730_153912.jpg differ
diff --git a/photos/IMG_20200730_154429.jpg b/photos/IMG_20200730_154429.jpg
new file mode 100644
index 0000000..e320b7e
Binary files /dev/null and b/photos/IMG_20200730_154429.jpg differ
diff --git a/photos/IMG_20200730_154647.jpg b/photos/IMG_20200730_154647.jpg
new file mode 100644
index 0000000..2e1a8fe
Binary files /dev/null and b/photos/IMG_20200730_154647.jpg differ
diff --git a/photos/IMG_20200730_154816.jpg b/photos/IMG_20200730_154816.jpg
new file mode 100644
index 0000000..4929b51
Binary files /dev/null and b/photos/IMG_20200730_154816.jpg differ
diff --git a/photos/VID_20200730_154828.mp4 b/photos/VID_20200730_154828.mp4
new file mode 100644
index 0000000..bff3f82
Binary files /dev/null and b/photos/VID_20200730_154828.mp4 differ
diff --git a/photos/cpxground.jpg b/photos/cpxground.jpg
new file mode 100644
index 0000000..2d6f2d8
Binary files /dev/null and b/photos/cpxground.jpg differ
diff --git a/photos/cpxsat.jpg b/photos/cpxsat.jpg
new file mode 100644
index 0000000..edbc16e
Binary files /dev/null and b/photos/cpxsat.jpg differ
diff --git a/photos/smoke.gif b/photos/smoke.gif
new file mode 100644
index 0000000..b14b16e
Binary files /dev/null and b/photos/smoke.gif differ