├── .gitignore ├── LICENSE ├── README.md ├── firmware ├── corona_lamp │ ├── .clang-format │ ├── Colors.h │ ├── Date.h │ ├── Optional.h │ ├── README.md │ ├── Utilities.h │ └── corona_lamp.ino ├── gerrit_watcher │ └── gerrit_watcher.ino └── github_watcher │ ├── github_reviews_watcher.py │ └── github_watcher.ino └── hardware └── gerrit_lamp ├── .gitignore ├── gerrit_lamp.brd └── gerrit_lamp.sch /.gitignore: -------------------------------------------------------------------------------- 1 | credentials.h 2 | .vscode 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dimitris Platis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Review Lamp 2 | A colorful lamp to notify the developer team for pending code reviews. 3 | ![Code Review Lamp](https://i.imgur.com/V9rwpnD.jpg) 4 | 5 | ## What? 6 | Code Review Lamp is a Neopixel-based, WiFi-enabled gadget that reminds developers to peer-review their colleagues' code. For each submission that has not been reviewed enough, it dims up and down at a color that is specific to the developer who is trying to introduce a new functionality to a project. The lamp stops shining once the code has either received enough reviews by the team, been merged or designated as *Work In Progress*. Currently, it is configured to work with [Gerrit](https://www.gerritcodereview.com/) (v.2.15) or GitHub, but it could be programmed to fetch data from different tools such as Jenkins, GitLab etc. 7 | 8 | The gadget is comprised of an [ESP8266 microcontroller module](https://wiki.wemos.cc/products:d1:d1_mini) that connects wirelessly to the internet, as well as a [Neopixel ring](https://www.adafruit.com/product/1463) which displays various colors. 9 | 10 | ## Why? 11 | At work, we are a fast-paced team that likes to commit (relatively) small and often. Every commit does not only have to pass the various tests that are being run by our Continuous Integration machinery, but also gets extensively code reviewed before allowed to be merged. Since we are *six* in total and our internal code of conduct dictates *two* positive approvals for each new submission, the lack of responsiveness when it comes to reviewing, can impede the development process and speed. 12 | 13 | In an ideal world, developers would check their notification emails and would proceed on with reviewing their peer's code. However, as they are too absorbed by their ongoing work and perhaps slightly overwhelmed by the amount of emails they receive, these emails tend to be ignored. The common thought is that someone else in the team is going to review the code, but often no one does. 14 | 15 | We have employed two different techniques so far to solve the problem. The first one includes shouting **CODE REVIEW** loudly and the second placing color-coded bean bags (I am not kidding :laughing:) on each others' desks. The third that hopes to rule them all is the Code Review Lamp! 16 | 17 | Its advantage is that it continuously, and discretely, reminds developers that **the team** needs to review code and thus someone's work is being blocked as long as the lamp is shining. Additionally, they can see who is requesting the code review and how many reviews need to be conducted. 18 | 19 | ## How? 20 | Reaping the benefits of the Code Review Lamp is simple for the development team member. They merely need to push code to Gerrit, add the group which includes all team members to the review and set the review to *Work in Progress* or *Ready for review* depending on whether they want the team to start or stop reviewing the code. 21 | 22 | We have included a special user in our team's Gerrit group, representing the lamp, which is added along the team to each review. This user is being utilized by an ESP8266 microcontroller which connects to WiFi and [queries](https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html) Gerrit for open reviews where this "lamp user" is a reviewer. Of course, it is not necessary to have a separate user for the lamp. Any team member's already existing account can be used and this way Code Review Lamps can be personalized. However, since we consider code reviews a *team activity* and the foundation of cross-functionality and knowledge-sharing, we have opted for the "team approach". 23 | 24 | Next, for each review the user is assigned to, we determine how many reviews have been conducted and if they fall short of our agreed threshold, a color that corresponds to the *owner* of the review is displayed on the Neopixel ring. 25 | 26 | ### Software 27 | The [Gerrit firmware](https://github.com/platisd/code-review-lamp/blob/master/firmware/gerrit_watcher/gerrit_watcher.ino), compatible with ESP8266 microcontrollers, utilizes the [Adafruit Neopixel](https://github.com/adafruit/Adafruit_NeoPixel) library to control the Neopixel ring. When using **Gerrit**, the JSON response is parsed manually, as existing solutions which would nicely serialize the whole stream would frequently cause heap overflow. 28 | 29 | The [GitHub firmware](https://github.com/platisd/code-review-lamp/blob/master/firmware/github_watcher/github_watcher.ino) on the other hand does not parse the JSON response coming from **GitHub** itself directly. This is due to the API response being overly verbose for the ESP8266 microcontroller, which renders parsing it on the microcontroller infeasible. Instead, an external server has to be used which will process the data and merely send what colors should be displayed. An example of such a server, written in Python3, can be found [here](https://github.com/platisd/code-review-lamp/blob/master/firmware/github_watcher/github_reviews_watcher.py). 30 | 31 | ### Hardware 32 | The gadget is rather simple to source and assemble. The only "custom" part is the [PCB](https://github.com/platisd/code-review-lamp/tree/master/hardware/gerrit_lamp) which merely connects the ESP8266 module with the Neopixel ring. 33 | 34 | ### Get started 35 | After you have assembled the hardware, flash the firmware by following the steps below: 36 | * If on Windows or Mac, download the [Serial to USB chip drivers](https://wiki.wemos.cc/downloads) 37 | * Download and install the latest [Arduino IDE](https://www.arduino.cc/en/Main/Software) for your distribution 38 | * Install the `Adafruit Neopixel` library 39 | * In Arduino IDE, click on `Sketch` :arrow_right: `Include Library` :arrow_right: `Manage Libraries` 40 | * Look for `Adafruit NeoPixel` and install the `Adafruit NeoPixel by adafruit` 41 | * Install the ESP8266 SDK 42 | * In Arduino IDE, click on `File` :arrow_right: `Preferences` :arrow_right: `Additional Board Manager URLs` 43 | * Paste `http://arduino.esp8266.com/stable/package_esp8266com_index.json` and click `OK` 44 | * In Arduino IDE, click on `Tools` :arrow_right: `Board` :arrow_right: `Boards Manager` 45 | * Look for `esp8266` and install the `esp8266 by ESP8266 Community` 46 | * Select the Wemos D1 Mini board 47 | * In Arduino IDE, click on `Tools` :arrow_right: `Board` :arrow_right: `LOLIN(WEMOS) D1 R2 & mini` 48 | * Select the serial port your Code Review Lamp is connected to 49 | * In Arduino IDE, click on `Tools` :arrow_right: `Port` 50 | * Copy & paste the [code](https://github.com/platisd/code-review-lamp/blob/master/firmware/gerrit_watcher/gerrit_watcher.ino) to your IDE 51 | * Make the necessary adjustments for your own SSID, username etc 52 | * GitHub Token 53 | * Create a [personal access token](https://github.com/settings/tokens) 54 | * Give it all `repo` permissions if you want it to access your private repositories too, otherwise just `public_repo` 55 | * Upload the firmware by clicking `Upload` (the right arrow button on the upper left corner of your IDE) 56 | 57 | ### How to use 58 | * Commit your change to Gerrit or GitHub 59 | * Add the group which includes the team as reviewers 60 | * A special "notification" user that will trigger the lamp should be part of the group 61 | * ??? 62 | * Get your code reviewed faster! 63 | 64 | ## Components 65 | * [Code Review Lamp PCB](https://www.pcbway.com/project/shareproject/W17435BSW42_code_review_lamp.html) 66 | * [3D printed case](https://www.tinkercad.com/things/evNud1d8GYI) 67 | * [16 RGB LED Neopixel Ring](https://www.adafruit.com/product/1463) 68 | * [Wemos D1 Mini](https://wiki.wemos.cc/products:d1:d1_mini) 69 | * 330Ohm resistor 70 | * 470uF/16V capacitor 71 | * M3x40mm screws (4) 72 | * M3 nuts (4) 73 | * Micro USB cable 74 | 75 | ## Media 76 | * Article on [platis.solutions](https://platis.solutions/blog/2018/09/26/code-review-lamp/) 77 | * [Demo video](https://www.youtube.com/watch?v=TPO2nQkfprY) 78 | -------------------------------------------------------------------------------- /firmware/corona_lamp/.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | BasedOnStyle: Mozilla 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveAssignments: true 6 | AlignConsecutiveDeclarations: false 7 | AlignEscapedNewlinesLeft: true 8 | AlignOperands: true 9 | AlignTrailingComments: true 10 | AllowAllParametersOfDeclarationOnNextLine: true 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortCaseLabelsOnASingleLine: false 13 | AllowShortFunctionsOnASingleLine: Empty 14 | AllowShortIfStatementsOnASingleLine: false 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: true 20 | BinPackArguments: false 21 | BinPackParameters: false 22 | BraceWrapping: 23 | AfterClass: false 24 | AfterControlStatement: false 25 | AfterEnum: false 26 | AfterFunction: true 27 | AfterNamespace: false 28 | AfterObjCDeclaration: true 29 | AfterStruct: true 30 | AfterUnion: false 31 | BeforeCatch: false 32 | BeforeElse: false 33 | IndentBraces: false 34 | BreakBeforeBinaryOperators: All 35 | BreakBeforeBraces: Allman 36 | BreakBeforeTernaryOperators: true 37 | BreakConstructorInitializersBeforeComma: false 38 | ColumnLimit: 120 39 | CommentPragmas: '^ IWYU pragma:' 40 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 41 | ConstructorInitializerIndentWidth: 4 42 | ContinuationIndentWidth: 4 43 | Cpp11BracedListStyle: true 44 | DerivePointerAlignment: false 45 | DisableFormat: false 46 | ExperimentalAutoDetectBinPacking: false 47 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 48 | IncludeCategories: 49 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 50 | Priority: 2 51 | - Regex: '^(<|"(gtest|isl|json)/)' 52 | Priority: 3 53 | - Regex: '.*' 54 | Priority: 1 55 | IndentCaseLabels: false 56 | IndentWidth: 4 57 | IndentWrappedFunctionNames: false 58 | KeepEmptyLinesAtTheStartOfBlocks: true 59 | MacroBlockBegin: '' 60 | MacroBlockEnd: '' 61 | MaxEmptyLinesToKeep: 1 62 | NamespaceIndentation: None 63 | ObjCBlockIndentWidth: 4 64 | ObjCSpaceAfterProperty: true 65 | ObjCSpaceBeforeProtocolList: true 66 | PenaltyBreakBeforeFirstCallParameter: 19 67 | PenaltyBreakComment: 300 68 | PenaltyBreakFirstLessLess: 120 69 | PenaltyBreakString: 1000 70 | PenaltyExcessCharacter: 1000000 71 | PenaltyReturnTypeOnItsOwnLine: 60 72 | PointerAlignment: Left 73 | ReflowComments: true 74 | SortIncludes: true 75 | SpaceAfterCStyleCast: false 76 | SpaceBeforeAssignmentOperators: true 77 | SpaceBeforeParens: ControlStatements 78 | SpaceInEmptyParentheses: false 79 | SpacesBeforeTrailingComments: 1 80 | SpacesInAngles: false 81 | SpacesInContainerLiterals: true 82 | SpacesInCStyleCastParentheses: false 83 | SpacesInParentheses: false 84 | SpacesInSquareBrackets: false 85 | Standard: Cpp11 86 | TabWidth: 4 87 | UseTab: Never 88 | FixNamespaceComments: true -------------------------------------------------------------------------------- /firmware/corona_lamp/Colors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace corona_lamp 4 | { 5 | 6 | struct RGBColor 7 | { 8 | RGBColor(int r = 0, int g = 0, int b = 0) 9 | : red{r} 10 | , green{g} 11 | , blue{b} 12 | { 13 | } 14 | int red; 15 | int green; 16 | int blue; 17 | }; 18 | 19 | struct HSVColor 20 | { 21 | /** 22 | @param h Hue ranged [0,360) 23 | @param s Saturation ranged [0,100] 24 | @param v Value ranged [0,100] 25 | */ 26 | HSVColor(int h = 0, int s = 0, int v = 0) 27 | : hue{h} 28 | , saturation{s} 29 | , value{v} 30 | { 31 | } 32 | int hue; 33 | int saturation; 34 | int value; 35 | 36 | /** 37 | Converts HSV colors to RGB that can be used for Neopixels 38 | so that we can adjust the brightness of the colors. 39 | Code adapted from: https://stackoverflow.com/a/14733008 40 | 41 | @param hsv The color in HSV format to convert 42 | @return The equivalent color in RGB 43 | */ 44 | RGBColor toRGB() const 45 | { 46 | // Scale the HSV values to the expected range 47 | auto rangedHue = map(hue, 0, 359, 0, 255); 48 | auto rangedSat = map(saturation, 0, 100, 0, 255); 49 | auto rangedVal = map(value, 0, 100, 0, 255); 50 | 51 | if (rangedSat == 0) 52 | { 53 | return {rangedVal, rangedVal, rangedVal}; 54 | } 55 | 56 | auto region = rangedHue / 43; 57 | auto remainder = (rangedHue - (region * 43)) * 6; 58 | 59 | auto p = (rangedVal * (255 - rangedSat)) >> 8; 60 | auto q = (rangedVal * (255 - ((rangedSat * remainder) >> 8))) >> 8; 61 | auto t = (rangedVal * (255 - ((rangedSat * (255 - remainder)) >> 8))) >> 8; 62 | 63 | switch (region) 64 | { 65 | case 0: 66 | return {rangedVal, t, p}; 67 | case 1: 68 | return {q, rangedVal, p}; 69 | case 2: 70 | return {p, rangedVal, t}; 71 | case 3: 72 | return {p, q, rangedVal}; 73 | case 4: 74 | return {t, p, rangedVal}; 75 | default: 76 | return {rangedVal, p, q}; 77 | } 78 | } 79 | }; 80 | 81 | } // namespace corona_lamp 82 | -------------------------------------------------------------------------------- /firmware/corona_lamp/Date.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace corona_lamp 4 | { 5 | struct Date 6 | { 7 | Date(int y, int m, int d) 8 | : year{y} 9 | , month{m} 10 | , day{d} 11 | { 12 | } 13 | 14 | operator String() const 15 | { 16 | return String(month) + String("-") + String(day) + String("-") + String(year); 17 | } 18 | 19 | int year; 20 | int month; 21 | int day; 22 | }; 23 | } // namespace corona_lamp 24 | -------------------------------------------------------------------------------- /firmware/corona_lamp/Optional.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace corona_lamp 6 | { 7 | struct UninitializedOptional 8 | { 9 | }; 10 | 11 | static const UninitializedOptional Nullopt; 12 | 13 | template 14 | class Optional 15 | { 16 | public: 17 | Optional() = default; 18 | Optional(const UninitializedOptional&) {} 19 | 20 | Optional(T&& t) 21 | : valid{true} 22 | , optionalValue{t} 23 | { 24 | } 25 | 26 | T value() const 27 | { 28 | assert(valid); 29 | return optionalValue; 30 | } 31 | 32 | bool operator!() const 33 | { 34 | return !valid; 35 | } 36 | 37 | void operator=(const T&& t) 38 | { 39 | optionalValue = t; 40 | valid = true; 41 | } 42 | 43 | private: 44 | bool valid{false}; 45 | T optionalValue; 46 | }; 47 | 48 | } // namespace corona_lamp 49 | -------------------------------------------------------------------------------- /firmware/corona_lamp/README.md: -------------------------------------------------------------------------------- 1 | # Corona lamp 2 | Corona lamp is a twist over the [Code Review lamp](https://github.com/platisd/code-review-lamp) 3 | to raise awareness over the spread of the [CoViD-19](https://en.wikipedia.org/wiki/Coronavirus_disease_2019). 4 | 5 | ![corona lamp](https://i.imgur.com/7pOjvkv.jpg) 6 | 7 | ## What? 8 | 9 | Corona lamp illuminates according to the growth rate of the CoViD-19 infections in the selected country, 10 | over the past 2 weeks. Overall, the more red the lamp is, the higher the infection rate. When it gets 11 | greener, it means that the growth rate of the disease is low or has even stopped. 12 | 13 | There are **16** LEDs on the lamp, each illuminated for the disease's growth rate on that particular 14 | day. The data are fetched wirelessly, via a ESP8266 microcontroller and their original source currently 15 | is Center for Systems Science and Engineering 16 | ([CSSE](https://www.arcgis.com/apps/opsdashboard/index.html#/bda7594740fd40299423467b48e9ecf6)) at 17 | Johns Hopkins University. 18 | 19 | ## Why? 20 | 21 | Corona lamp, can be utilized to quickly monitor the progress of the country's efforts to halt 22 | the disease, without being as *threatening* as graphs and sometimes misleading as plain numbers. 23 | 24 | 25 | ## How? 26 | 27 | New data are fetched via the [covid-api](https://covid-api.quintessential.gr/) by 28 | [Quintessential SFT](https://www.quintessential.gr/) every 12 hours. The source code for the service 29 | can be found [here](https://github.com/Quintessential-SFT/Covid-19-API/). 30 | 31 | Then, the growth rate for each day over the last (approximately) two weeks is calcutated and 32 | assigned to a color. 33 | 34 | ### Dependencies 35 | 36 | Fetch the following resources via the Arduino IDE library manager: 37 | 38 | * [ArduinoJson](https://arduinojson.org/) 39 | * [NTPClient](https://github.com/arduino-libraries/NTPClient) 40 | 41 | ### Personalize 42 | 43 | To personalize the Corona Lamp for your country, simply change the `kCountry` 44 | [value](https://github.com/platisd/code-review-lamp/blob/master/firmware/corona_lamp/corona_lamp.ino#L29) 45 | accordingly. A list with the available countries can be found [here](https://covid-api.quintessential.gr/data/meta/countries). 46 | -------------------------------------------------------------------------------- /firmware/corona_lamp/Utilities.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace corona_lamp 4 | { 5 | template 6 | constexpr T getMap(const T& valueToMap, const T& fromLow, const T& fromHigh, const T& toLow, const T& toHigh) 7 | { 8 | return fromHigh == fromLow ? toLow : (valueToMap - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; 9 | } 10 | } // namespace corona_lamp 11 | -------------------------------------------------------------------------------- /firmware/corona_lamp/corona_lamp.ino: -------------------------------------------------------------------------------- 1 | #include "Colors.h" 2 | #include "Date.h" 3 | #include "Optional.h" 4 | #include "Utilities.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace corona_lamp; 17 | 18 | const auto kSsid = "your-ssid"; 19 | const auto kPassword = "your-password"; 20 | const auto kNeopixelPin = 15; 21 | const auto kNeopixelRingSize = 16; 22 | const auto kErrorBlinkInterval = 250UL; 23 | const auto kReconnectTimeout = 100UL; 24 | const auto kRetryConnectionInterval = 500UL; 25 | const auto kConnectionRetries = 20; 26 | const auto kOneMinute = 1000UL * 60UL; 27 | const auto kWifiReconnectInterval = 30000UL; 28 | 29 | const auto kCountry = "sweden"; 30 | const char kFingerprint[] PROGMEM = "07 62 08 46 01 4E 07 CB 68 AB 12 53 A8 5E 6F 7E D4 D3 4E 20"; 31 | 32 | const HSVColor MELLOW_YELLOW(40, 100, 100); 33 | 34 | Adafruit_NeoPixel ring(kNeopixelRingSize, kNeopixelPin, NEO_GRB + NEO_KHZ800); 35 | WiFiUDP ntpUDP; 36 | NTPClient timeClient(ntpUDP); 37 | 38 | /** 39 | Display an error pattern on the neopixels 40 | @param neopixels The neopixels to display the error 41 | */ 42 | void indicateError(Adafruit_NeoPixel& neopixels) 43 | { 44 | // Blink red LEDs sequentially to indicate an error 45 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) 46 | { 47 | neopixels.setPixelColor(pixel, 200, 0, 0); 48 | neopixels.show(); 49 | delay(kErrorBlinkInterval); 50 | neopixels.setPixelColor(pixel, 0, 0, 0); 51 | neopixels.show(); 52 | delay(kErrorBlinkInterval); 53 | } 54 | } 55 | 56 | /** 57 | (Re)connects the module to WiFi 58 | */ 59 | void connectToWifi() 60 | { 61 | // Set WiFi to station mode & disconnect from an AP if previously connected 62 | WiFi.mode(WIFI_STA); 63 | WiFi.disconnect(); 64 | delay(kReconnectTimeout); 65 | 66 | // Try to connect to the internet 67 | WiFi.begin(kSsid, kPassword); 68 | auto attemptsLeft = kConnectionRetries; 69 | Serial.print("Connecting"); 70 | while ((WiFi.status() != WL_CONNECTED) && (--attemptsLeft > 0)) 71 | { 72 | delay(kRetryConnectionInterval); // Wait a bit before retrying 73 | Serial.print("."); 74 | } 75 | 76 | if (attemptsLeft <= 0) 77 | { 78 | Serial.println(" Connection error!"); 79 | indicateError(ring); 80 | } 81 | else 82 | { 83 | Serial.println(" Connection success"); 84 | } 85 | } 86 | 87 | std::vector getLatestDatePeriod(int days) 88 | { 89 | static const auto secondsInDay = 86400UL; 90 | timeClient.update(); 91 | // Get yesterday's data to ensure we are never too early to fetch data for the day 92 | const auto yesterday = timeClient.getEpochTime() - secondsInDay; 93 | std::vector period; 94 | 95 | for (auto i = days - 1; i >= 0; i--) 96 | { 97 | 98 | std::time_t t(yesterday - (i * secondsInDay)); 99 | const auto now = std::localtime(&t); 100 | period.emplace_back(Date(now->tm_year + 1900, now->tm_mon + 1, now->tm_mday)); 101 | } 102 | 103 | return period; 104 | } 105 | 106 | Optional getInfectionsUntil(const Date& date, const char* country) 107 | { 108 | WiFiClientSecure client; 109 | client.setFingerprint(kFingerprint); 110 | 111 | static const auto host = "covid-api.quintessential.gr"; 112 | static const auto port = 443; 113 | 114 | if (!client.connect(host, port)) 115 | { 116 | Serial.println("connection failed"); 117 | return Nullopt; 118 | } 119 | 120 | static const String baseUrl = "/data/custom?"; 121 | const String query = baseUrl + String("country=") + String(country) + String("&date=") + String(date); 122 | 123 | const auto getRequest = String("GET ") + query + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" 124 | + "User-Agent: BuildFailureDetectorESP8266\r\n" + "Connection: close\r\n\r\n"; 125 | client.print(getRequest); 126 | 127 | // Go through the headers before we can deserialize the JSON 128 | while (client.connected()) 129 | { 130 | String line = client.readStringUntil('\n'); 131 | if (line == "\r") 132 | { 133 | break; 134 | } 135 | } 136 | 137 | DynamicJsonDocument doc(600); 138 | deserializeJson(doc, client); 139 | const auto root = doc.as(); 140 | const auto payload = root[0]; 141 | static const auto infectedKey = "Confirmed"; 142 | 143 | Serial.print("Date: "); 144 | Serial.print(date); 145 | Serial.print("\t\t"); 146 | 147 | if (!payload.containsKey(infectedKey)) 148 | { 149 | Serial.println("No data"); 150 | return Nullopt; 151 | } 152 | 153 | Optional infections = payload[infectedKey].as(); 154 | Serial.println(infections.value()); 155 | 156 | return infections; 157 | } 158 | 159 | std::vector> getTotalInfectionsPerDayFor(const std::vector& period) 160 | { 161 | std::vector> totalInfectionsPerDay; 162 | 163 | for (const auto& date : period) 164 | { 165 | totalInfectionsPerDay.emplace_back(getInfectionsUntil(date, kCountry)); 166 | } 167 | 168 | return totalInfectionsPerDay; 169 | } 170 | 171 | std::vector> getInfectionGrowthRates(const std::vector>& infections) 172 | { 173 | std::vector> infectionGrowthRates(infections.size() - 1); 174 | 175 | for (auto i = 1; i < infections.size(); i++) 176 | { 177 | const auto indexToFill = i - 1; 178 | if (!!infections.at(i)) 179 | { 180 | if (!!infections.at(i - 1)) 181 | { 182 | const auto currentInfection = infections.at(i).value(); 183 | const auto previousInfection = infections.at(i - 1).value(); 184 | const auto infectionDelta = currentInfection - previousInfection; 185 | const auto noGrowth = infectionDelta == 0; 186 | const Optional growthRate 187 | = noGrowth ? 0.0f : static_cast(infectionDelta) / static_cast(previousInfection); 188 | infectionGrowthRates[indexToFill] = growthRate; 189 | } 190 | else 191 | { 192 | // If there are no previous data, assume an 100% growth rate 193 | infectionGrowthRates[indexToFill] = Optional(1); 194 | } 195 | } 196 | else 197 | { 198 | infectionGrowthRates[indexToFill] = Nullopt; 199 | } 200 | } 201 | 202 | return infectionGrowthRates; 203 | } 204 | 205 | std::vector getGrowthColors(const std::vector>& growthRates) 206 | { 207 | static const auto baseColor = MELLOW_YELLOW; 208 | std::vector colors; 209 | 210 | for (const auto& growthRate : growthRates) 211 | { 212 | auto color = baseColor; 213 | if (!growthRate) 214 | { 215 | color = HSVColor(0, 0, 0); 216 | } 217 | else 218 | { 219 | static const auto okGrowthRate = 0.05f; 220 | const auto rate = constrain(growthRate.value(), 0.0f, 1.0f); 221 | const auto hueDrift = rate <= okGrowthRate ? getMap(rate, 0.0f, okGrowthRate, 100.0f, 60.0f) 222 | : getMap(rate, okGrowthRate, 1.0f, 20.0f, 0.0f); 223 | color.hue = hueDrift; 224 | Serial.print("Growth: "); 225 | Serial.print(growthRate.value()); 226 | } 227 | Serial.print("\t\tHue: "); 228 | Serial.println(color.hue); 229 | colors.emplace_back(color); 230 | } 231 | 232 | return colors; 233 | } 234 | 235 | void showClockEffect(Adafruit_NeoPixel& neopixels, const std::vector& colors) 236 | { 237 | assert(neopixels.numPixels() <= colors.size()); 238 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) 239 | { 240 | const auto color = colors.at(pixel).toRGB(); 241 | neopixels.setPixelColor(pixel, color.red, color.green, color.blue); 242 | neopixels.show(); 243 | delay(100); 244 | } 245 | } 246 | 247 | void dimDown(Adafruit_NeoPixel& neopixels, const std::vector& colors) 248 | { 249 | assert(neopixels.numPixels() <= colors.size()); 250 | for (auto value = 100; value >= 0; value--) 251 | { 252 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) 253 | { 254 | const auto color = HSVColor(colors.at(pixel).hue, colors.at(pixel).saturation, value).toRGB(); 255 | neopixels.setPixelColor(pixel, color.red, color.green, color.blue); 256 | } 257 | neopixels.show(); 258 | delay(100); 259 | } 260 | } 261 | 262 | void dimUp(Adafruit_NeoPixel& neopixels, const std::vector& colors) 263 | { 264 | assert(neopixels.numPixels() <= colors.size()); 265 | for (auto value = 0; value <= 100; value++) 266 | { 267 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) 268 | { 269 | const auto color = HSVColor(colors.at(pixel).hue, colors.at(pixel).saturation, value).toRGB(); 270 | neopixels.setPixelColor(pixel, color.red, color.green, color.blue); 271 | } 272 | neopixels.show(); 273 | delay(100); 274 | } 275 | } 276 | 277 | void setup() 278 | { 279 | Serial.begin(115200); 280 | ring.begin(); 281 | ring.show(); // Initialize all pixels to 'off' 282 | connectToWifi(); 283 | timeClient.begin(); 284 | } 285 | 286 | void loop() 287 | { 288 | if (WiFi.status() != WL_CONNECTED) 289 | { 290 | connectToWifi(); 291 | delay(kWifiReconnectInterval); 292 | } 293 | else 294 | { 295 | const auto daysToStudy = kNeopixelRingSize + 1; // Number of pixels available + previous day 296 | const auto period = getLatestDatePeriod(daysToStudy); 297 | const auto infectionsPerDay = getTotalInfectionsPerDayFor(period); 298 | const auto infectionGrowthRates = getInfectionGrowthRates(infectionsPerDay); 299 | const auto colors = getGrowthColors(infectionGrowthRates); 300 | for (auto hours = 0; hours < 12; hours++) 301 | { 302 | showClockEffect(ring, colors); 303 | for (auto minutes = 0; hours < 60; minutes++) 304 | { 305 | dimDown(ring, colors); 306 | dimUp(ring, colors); 307 | delay(kOneMinute); 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /firmware/gerrit_watcher/gerrit_watcher.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Keep your project specific credentials in a non-version controlled 8 | // `credentials.h` file and set the `NO_CREDENTIALS_HEADER` to false. 9 | #define NO_CREDENTIALS_HEADER false 10 | #if NO_CREDENTIALS_HEADER == true 11 | const auto INTERNET_SSID = "your_ssid"; 12 | const auto PASSWORD = "your_password"; 13 | const String GERRIT_URL = "http://your_gerrit_url:8080"; 14 | const auto GERRIT_USERNAME = "your_gerrit_username"; 15 | const auto GERRIT_HTTP_PASSWORD = "your_gerrit_http_password"; 16 | #else 17 | #include "credentials.h" 18 | #endif 19 | 20 | enum class Effect {PULSE, RADAR, COOL_RADAR}; 21 | const auto MY_EFFECT = Effect::RADAR; 22 | 23 | struct RGBColor { 24 | RGBColor(int r = 0, int g = 0, int b = 0) : red{r}, green{g}, blue{b} {} 25 | int red; 26 | int green; 27 | int blue; 28 | }; 29 | 30 | struct HSVColor { 31 | /** 32 | @param h Hue ranged [0,360) 33 | @param s Saturation ranged [0,100] 34 | @param v Value ranged [0,100] 35 | */ 36 | HSVColor(int h = 0, int s = 0, int v = 0) : hue{h}, saturation{s}, value{v} {} 37 | int hue; 38 | int saturation; 39 | int value; 40 | 41 | /** 42 | Converts HSV colors to RGB that can be used for Neopixels 43 | so that we can adjust the brightness of the colors. 44 | Code adapted from: https://stackoverflow.com/a/14733008 45 | 46 | @param hsv The color in HSV format to convert 47 | @return The equivalent color in RGB 48 | */ 49 | RGBColor toRGB() const { 50 | // Scale the HSV values to the expected range 51 | auto rangedHue = map(hue, 0, 359, 0, 255); 52 | auto rangedSat = map(saturation, 0, 100, 0, 255); 53 | auto rangedVal = map(value, 0, 100, 0, 255); 54 | 55 | if (rangedSat == 0) { 56 | return {rangedVal, rangedVal, rangedVal}; 57 | } 58 | 59 | auto region = rangedHue / 43; 60 | auto remainder = (rangedHue - (region * 43)) * 6; 61 | 62 | auto p = (rangedVal * (255 - rangedSat)) >> 8; 63 | auto q = (rangedVal * (255 - ((rangedSat * remainder) >> 8))) >> 8; 64 | auto t = (rangedVal * (255 - ((rangedSat * (255 - remainder)) >> 8))) >> 8; 65 | 66 | switch (region) { 67 | case 0: 68 | return {rangedVal, t, p}; 69 | case 1: 70 | return {q, rangedVal, p}; 71 | case 2: 72 | return {p, rangedVal, t}; 73 | case 3: 74 | return {p, q, rangedVal}; 75 | case 4: 76 | return {t, p, rangedVal}; 77 | default: 78 | return {rangedVal, p, q}; 79 | } 80 | } 81 | }; 82 | 83 | const auto NEOPIXEL_PIN = 15; 84 | const auto NEOPIXEL_RING_SIZE = 16; 85 | const auto DIM_WINDOW = 10000UL; 86 | const auto CHECK_FOR_REVIEWS_INTERVAL = 20000UL; 87 | const auto ERROR_BLINK_INTERVAL = 250UL; 88 | const auto WAIT_FOR_GERRIT_RESPONSE = 50UL; 89 | const auto RECONNECT_TIMEOUT = 100UL; 90 | const auto RETRY_CONNECTION_INTERVAL = 500UL; 91 | const auto CONNECTION_RETRIES = 20; 92 | const auto OPEN_REVIEWS_QUERY = "/a/changes/?q=status:open+is:reviewer"; 93 | const String CHANGES_ENDPOINT = GERRIT_URL + "/a/changes/"; 94 | const auto REVIEWERS = "/reviewers/"; 95 | const auto DELETE = "/delete"; 96 | const auto ALL_REVIEWS_ASSIGNED_URL = GERRIT_URL + OPEN_REVIEWS_QUERY; 97 | const auto GERRIT_REVIEW_NUMBER_ATTRIBUTE = "_number"; 98 | const auto GERRIT_REVIEW_APPROVAL_ATTRIBUTE = "Code-Review"; 99 | const auto GERRIT_REVIEW_OWNERID_ATTRIBUTE = "_account_id"; 100 | const auto ENOUGH_CONDUCTED_REVIEWS = 2; 101 | 102 | const HSVColor KINDA_ORANGE (10, 100, 100); 103 | const HSVColor MELLOW_YELLOW (30, 100, 100); 104 | const HSVColor ALIEN_GREEN (100, 100, 100); 105 | const HSVColor ALMOST_WHITE (293, 4, 70); 106 | const HSVColor GREEK_BLUE (227, 100, 100); 107 | const HSVColor GOTH_PURPLE (315, 100, 100); 108 | const HSVColor BLOOD_RED (0, 100, 100); 109 | 110 | Adafruit_NeoPixel ring(NEOPIXEL_RING_SIZE, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); 111 | 112 | /** 113 | Maps Gerrit user IDs to lamp colors, adapt accordingly. 114 | @param userId Gerrit user ID 115 | @return The HSV color to match the specific user 116 | */ 117 | HSVColor toColor(const String& userId) { 118 | switch (userId.toInt()) { 119 | case 1000037: // nm 120 | return MELLOW_YELLOW; 121 | case 1000079: // dj 122 | return ALIEN_GREEN; 123 | case 1000078: // jk 124 | return GOTH_PURPLE; 125 | case 1000039: // nj 126 | return KINDA_ORANGE; 127 | case 1000036: // dp 128 | return BLOOD_RED; 129 | case 1000354: // fb 130 | return GREEK_BLUE; 131 | default: // Developer from another team 132 | return ALMOST_WHITE; 133 | } 134 | } 135 | 136 | /** 137 | Display an error pattern on the neopixels 138 | @param neopixels The neopixels to display the error 139 | */ 140 | void indicateError(Adafruit_NeoPixel& neopixels) { 141 | // Blink red LEDs sequentially to indicate an error 142 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) { 143 | neopixels.setPixelColor(pixel, 200, 0, 0); 144 | neopixels.show(); 145 | delay(ERROR_BLINK_INTERVAL); 146 | neopixels.setPixelColor(pixel, 0, 0, 0); 147 | neopixels.show(); 148 | delay(ERROR_BLINK_INTERVAL); 149 | } 150 | } 151 | 152 | /** 153 | @param url The URL to execute a GET request 154 | @param key The key to look inside the incoming JSON stream 155 | @return A list with all the values of the specific key 156 | */ 157 | std::vector getStreamAttribute(const String& url, const String& key) { 158 | HTTPClient http; 159 | http.begin(url); 160 | http.setAuthorization(GERRIT_USERNAME, GERRIT_HTTP_PASSWORD); 161 | auto httpCode = http.GET(); 162 | 163 | if (httpCode < 0 || httpCode != HTTP_CODE_OK) { 164 | Serial.printf("[%s] GET failed with code '%s' for key '%s'\r\n", __FUNCTION__, http.errorToString(httpCode).c_str(), key.c_str()); 165 | http.end(); 166 | return {}; 167 | } 168 | delay(WAIT_FOR_GERRIT_RESPONSE); 169 | 170 | auto documentLength = http.getSize(); 171 | auto stream = http.getStream(); 172 | 173 | std::vector keyValues; 174 | if (http.connected() && (documentLength > 0 || documentLength == -1)) { 175 | while (stream.available()) { 176 | // Parse the value of the key when found 177 | String line = stream.readStringUntil('\n'); 178 | line.trim(); 179 | line.replace("\"", ""); 180 | // Clear out unnecessary characters 181 | if (line.startsWith(key)) { 182 | line.replace(key, ""); 183 | line.replace(",", ""); 184 | line.replace(":", ""); 185 | line.trim(); 186 | // By now it should include just the information we are interested in 187 | keyValues.push_back(line); 188 | } 189 | } 190 | } 191 | 192 | if (keyValues.empty()) { 193 | Serial.printf("Warning - Key not found: %s\n\r", key.c_str()); 194 | } 195 | http.end(); 196 | 197 | return keyValues; 198 | } 199 | 200 | /** 201 | (Re)connects the module to WiFi 202 | */ 203 | void connectToWifi() { 204 | // Set WiFi to station mode & disconnect from an AP if previously connected 205 | WiFi.mode(WIFI_STA); 206 | WiFi.disconnect(); 207 | delay(RECONNECT_TIMEOUT); 208 | 209 | // Try to connect to the internet 210 | WiFi.begin(INTERNET_SSID, PASSWORD); 211 | auto attemptsLeft = CONNECTION_RETRIES; 212 | Serial.print("Connecting"); 213 | while ((WiFi.status() != WL_CONNECTED) && (--attemptsLeft > 0)) { 214 | delay(RETRY_CONNECTION_INTERVAL); // Wait a bit before retrying 215 | Serial.print("."); 216 | } 217 | 218 | if (attemptsLeft <= 0) { 219 | Serial.println(" Connection error!"); 220 | indicateError(ring); 221 | } else { 222 | Serial.println(" Connection success"); 223 | } 224 | } 225 | 226 | /** 227 | Get all open reviews that our gerrit user has been assigned to, 228 | then figure out whether enough developers have code-reviewed 229 | each review. If this has not happened, return a color 230 | (mapped to a developer) for every un-reviewed review. 231 | Once a review has been reviewed adequately, remove ourselves 232 | from it. 233 | 234 | @return HSV colors for every unfinished code review 235 | */ 236 | std::vector getColorsForUnfinishedReviews() { 237 | std::vector colorsToShow; 238 | Serial.println("Getting all reviews assigned to us"); 239 | auto reviews = getStreamAttribute(ALL_REVIEWS_ASSIGNED_URL, GERRIT_REVIEW_NUMBER_ATTRIBUTE); 240 | for (auto& review : reviews) { 241 | // Get all approvals for the specific review 242 | Serial.printf("Getting all approvals for review %s\n\r", review.c_str()); 243 | auto getChangeUrl = CHANGES_ENDPOINT + review; 244 | auto getReviewersUrl = CHANGES_ENDPOINT + review + REVIEWERS; 245 | auto approvals = getStreamAttribute(getReviewersUrl, GERRIT_REVIEW_APPROVAL_ATTRIBUTE); 246 | // Measure how many reviews have been conducted (i.e. approval is NOT `0`) 247 | auto conductedReviews = 0; 248 | for (auto& approval : approvals) { 249 | if (approval != "0") { 250 | conductedReviews++; 251 | } 252 | } 253 | 254 | if (conductedReviews < ENOUGH_CONDUCTED_REVIEWS) { 255 | auto ownerId = getStreamAttribute(getChangeUrl, GERRIT_REVIEW_OWNERID_ATTRIBUTE); 256 | if (!ownerId.empty()) { 257 | colorsToShow.push_back(toColor(ownerId.front())); 258 | } 259 | } else { 260 | Serial.printf("We got enough reviews in %s, no need to dim\n\r", review.c_str()); 261 | } 262 | } 263 | 264 | return colorsToShow; 265 | } 266 | 267 | /** 268 | Set the specified RGB color to all the pixels 269 | 270 | @param neopixels The neopixel structure to set color 271 | @param rgbColor The RGB color to set the pixels 272 | */ 273 | 274 | void setAllPixelColor(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 275 | switch (MY_EFFECT) { 276 | case Effect::RADAR: 277 | setRadarEffect(neopixels, rgbColor); 278 | break; 279 | case Effect::PULSE: 280 | setPulseEffect(neopixels, rgbColor); 281 | break; 282 | case Effect::COOL_RADAR: 283 | setCoolRadarEffect(neopixels, rgbColor); 284 | break; 285 | default: 286 | setRadarEffect(neopixels, rgbColor); 287 | break; 288 | } 289 | } 290 | 291 | /** 292 | Perform some radar effect 293 | @param neopixels The neopixel structure to set color 294 | @param rgbColor The RGB color to set the pixels 295 | */ 296 | void setRadarEffect(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 297 | const auto pixels = neopixels.numPixels(); 298 | static const auto SLICES = 3; 299 | static uint8_t startingPixel = 0; 300 | 301 | startingPixel++; // Does not matter if it rotates back to 0 302 | 303 | // Slice the lamp in parts where the first and brightest one is our radar effect 304 | // while the rest have a progressively dimmer color. 305 | for (auto slice = 0; slice < SLICES; slice++) { 306 | for (auto pixel = slice * pixels / SLICES + startingPixel; pixel < (slice + 1) * pixels / SLICES + startingPixel; pixel++) { 307 | neopixels.setPixelColor(pixel % pixels, rgbColor.red, rgbColor.green, rgbColor.blue); 308 | } 309 | 310 | rgbColor.red /= 2; 311 | rgbColor.green /= 2; 312 | rgbColor.blue /= 2; 313 | } 314 | } 315 | 316 | /** 317 | Perform some cool radar effect 318 | @param neopixels The neopixel structure to set color 319 | @param rgbColor The RGB color to set the pixels 320 | */ 321 | void setCoolRadarEffect(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 322 | const auto pixels = neopixels.numPixels(); 323 | static uint8_t startingPixel = 0; 324 | 325 | startingPixel++; // Does not matter if it rotates back to 0 326 | 327 | for (auto pixel = 0 + startingPixel; pixel < 3 * pixels / 5 + startingPixel; pixel++) { 328 | neopixels.setPixelColor(pixel % pixels, rgbColor.red, rgbColor.green, rgbColor.blue); 329 | } 330 | 331 | RGBColor rgb1; 332 | rgb1.red = rgbColor.green; 333 | rgb1.green = rgbColor.blue; 334 | rgb1.blue = rgbColor.red; 335 | 336 | for (auto pixel = 3 * pixels / 5 + startingPixel; pixel < 4 * pixels / 5 + startingPixel; pixel++) { 337 | neopixels.setPixelColor(pixel % pixels, rgb1.red, rgb1.green, rgb1.blue); 338 | } 339 | 340 | RGBColor rgb2; 341 | rgb2.red = rgbColor.blue; 342 | rgb2.green = rgbColor.red; 343 | rgb2.blue = rgbColor.green; 344 | 345 | for (auto pixel = 4 * pixels / 5 + startingPixel; pixel < pixels + startingPixel; pixel++) { 346 | neopixels.setPixelColor(pixel % pixels, rgb2.red, rgb2.green, rgb2.blue); 347 | } 348 | } 349 | 350 | /** 351 | Perform some pulse effect 352 | @param neopixels The neopixel structure to set color 353 | @param rgbColor The RGB color to set the pixels 354 | */ 355 | void setPulseEffect(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 356 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) { 357 | neopixels.setPixelColor(pixel, rgbColor.red, rgbColor.green, rgbColor.blue); 358 | } 359 | } 360 | 361 | /** 362 | Dim for all the supplied colors throughout the specified window 363 | @param neopixels The neopixel structure to dim 364 | @param hsvColors The HSV colors to be dimmed 365 | */ 366 | void dimWithColors(Adafruit_NeoPixel& neopixels, std::vector& hsvColors) { 367 | if (hsvColors.empty()) { 368 | Serial.println("All code is reviewed, good job"); 369 | return; 370 | } 371 | // Dim every color within the designated time window 372 | // The effect we are after is the more unfinished reviews 373 | // the faster the neopixels will dim 374 | auto timeSlotForEachColor = DIM_WINDOW / hsvColors.size(); 375 | for (const auto& hsvColor : hsvColors) { 376 | auto rgb = hsvColor.toRGB(); 377 | // The time slot has to be evenly divided among the intervals necessary 378 | // to dim it all the way up and down 379 | auto dimInterval = (timeSlotForEachColor / hsvColor.value) / 2; 380 | // Dim up every pixel for the current color 381 | for (auto intensity = 0; intensity < hsvColor.value; intensity++) { 382 | // Get the RGB value of the currently dimmed HSV color 383 | auto rgbColor = HSVColor(hsvColor.hue, hsvColor.saturation, intensity).toRGB(); 384 | setAllPixelColor(neopixels, rgbColor); 385 | neopixels.show(); 386 | delay(dimInterval); 387 | } 388 | 389 | // Dim down every pixel for the current color 390 | for (auto intensity = hsvColor.value; intensity >= 0; intensity--) { 391 | // Get the RGB value of the currently dimmed HSV color 392 | auto rgbColor = HSVColor(hsvColor.hue, hsvColor.saturation, intensity).toRGB(); 393 | setAllPixelColor(neopixels, rgbColor); 394 | neopixels.show(); 395 | delay(dimInterval); 396 | } 397 | } 398 | } 399 | 400 | void setup() { 401 | Serial.begin(9600); 402 | ring.begin(); 403 | ring.show(); // Initialize all pixels to 'off' 404 | connectToWifi(); 405 | } 406 | 407 | void loop() { 408 | if (WiFi.status() != WL_CONNECTED) { 409 | connectToWifi(); 410 | } else { 411 | auto hsvColors = getColorsForUnfinishedReviews(); 412 | dimWithColors(ring, hsvColors); 413 | } 414 | delay(CHECK_FOR_REVIEWS_INTERVAL); 415 | } 416 | -------------------------------------------------------------------------------- /firmware/github_watcher/github_reviews_watcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding:utf8 -*- 3 | ''' 4 | Gerrit Reviews Watcher 5 | A simple Flask server to fetch all pull request reviews for the specified user 6 | and return the respective colors for a Code Review Lamp to shine. 7 | ''' 8 | import json 9 | from flask import Flask, request, make_response 10 | import requests 11 | 12 | usernames_to_colors = {"colleague0": "red", "colleague1": "blue", 13 | "colleague2": "purple", "colleague3": "yellow", "colleague4": "orange", "colleague5": "green"} 14 | app = Flask("GitHub Review Requests Watcher") 15 | 16 | 17 | @app.route('/github_reviews//', methods=['GET']) 18 | def github_watcher(username, oauth_token): 19 | github_api_url = "https://api.github.com/" 20 | search_endpoint = "search/issues?q=is:open+is:pr+review-requested:" + \ 21 | username + "+archived:false" 22 | request_url = github_api_url + search_endpoint 23 | header_values = {"Accept": "application/vnd.github.v3+json", 24 | "Authorization": "token " + oauth_token, 25 | "User-Agent": "Code-Review-Lamp", "Connection": "close"} 26 | 27 | r = requests.get(url=request_url, headers=header_values) 28 | search_result = r.json() 29 | 30 | colors = str() 31 | for review_request in search_result["items"]: 32 | requester = review_request["user"]["login"] 33 | if requester in usernames_to_colors: 34 | colors += usernames_to_colors[requester] + "," 35 | else: 36 | colors += "white," 37 | 38 | return colors 39 | 40 | 41 | def main(): 42 | app.run() 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /firmware/github_watcher/github_watcher.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Keep your project specific credentials in a non-version controlled 8 | // `credentials.h` file and set the `NO_CREDENTIALS_HEADER` to false. 9 | #define NO_CREDENTIALS_HEADER false 10 | #if NO_CREDENTIALS_HEADER == true 11 | const auto INTERNET_SSID = "your_ssid"; 12 | const auto PASSWORD = "your_password"; 13 | const auto GITHUB_USERNAME = "your_GITHUB_USERNAME"; 14 | const auto GITHUB_OAUTH2_TOKEN = "your_GITHUB_OAUTH2_TOKEN"; 15 | const auto INTERMEDIATE_SERVER = "http://your.flask.server/"; 16 | #else 17 | #include "credentials.h" 18 | #endif 19 | 20 | enum class Effect {PULSE, RADAR, COOL_RADAR}; 21 | const auto MY_EFFECT = Effect::RADAR; 22 | 23 | struct RGBColor { 24 | RGBColor(int r = 0, int g = 0, int b = 0) : red{r}, green{g}, blue{b} {} 25 | int red; 26 | int green; 27 | int blue; 28 | }; 29 | 30 | struct HSVColor { 31 | /** 32 | @param h Hue ranged [0,360) 33 | @param s Saturation ranged [0,100] 34 | @param v Value ranged [0,100] 35 | */ 36 | HSVColor(int h = 0, int s = 0, int v = 0) : hue{h}, saturation{s}, value{v} {} 37 | int hue; 38 | int saturation; 39 | int value; 40 | 41 | /** 42 | Converts HSV colors to RGB that can be used for Neopixels 43 | so that we can adjust the brightness of the colors. 44 | Code adapted from: https://stackoverflow.com/a/14733008 45 | 46 | @param hsv The color in HSV format to convert 47 | @return The equivalent color in RGB 48 | */ 49 | RGBColor toRGB() const { 50 | // Scale the HSV values to the expected range 51 | auto rangedHue = map(hue, 0, 359, 0, 255); 52 | auto rangedSat = map(saturation, 0, 100, 0, 255); 53 | auto rangedVal = map(value, 0, 100, 0, 255); 54 | 55 | if (rangedSat == 0) { 56 | return {rangedVal, rangedVal, rangedVal}; 57 | } 58 | 59 | auto region = rangedHue / 43; 60 | auto remainder = (rangedHue - (region * 43)) * 6; 61 | 62 | auto p = (rangedVal * (255 - rangedSat)) >> 8; 63 | auto q = (rangedVal * (255 - ((rangedSat * remainder) >> 8))) >> 8; 64 | auto t = (rangedVal * (255 - ((rangedSat * (255 - remainder)) >> 8))) >> 8; 65 | 66 | switch (region) { 67 | case 0: 68 | return {rangedVal, t, p}; 69 | case 1: 70 | return {q, rangedVal, p}; 71 | case 2: 72 | return {p, rangedVal, t}; 73 | case 3: 74 | return {p, q, rangedVal}; 75 | case 4: 76 | return {t, p, rangedVal}; 77 | default: 78 | return {rangedVal, p, q}; 79 | } 80 | } 81 | }; 82 | 83 | const auto NEOPIXEL_PIN = 15; 84 | const auto NEOPIXEL_RING_SIZE = 16; 85 | const auto DIM_WINDOW = 10000UL; 86 | const auto CHECK_FOR_REVIEWS_INTERVAL = 20000UL; 87 | const auto ERROR_BLINK_INTERVAL = 250UL; 88 | const auto WAIT_FOR_GITHUB_RESPONSE = 50UL; 89 | const auto RECONNECT_TIMEOUT = 100UL; 90 | const auto RETRY_CONNECTION_INTERVAL = 500UL; 91 | const auto CONNECTION_RETRIES = 20; 92 | const auto REQUEST_URL = INTERMEDIATE_SERVER + String(GITHUB_USERNAME) + "/" + String(GITHUB_OAUTH2_TOKEN); 93 | 94 | const HSVColor KINDA_ORANGE (10, 100, 100); 95 | const HSVColor MELLOW_YELLOW (30, 100, 100); 96 | const HSVColor ALIEN_GREEN (100, 100, 100); 97 | const HSVColor ALMOST_WHITE (293, 4, 70); 98 | const HSVColor GREEK_BLUE (227, 100, 100); 99 | const HSVColor GOTH_PURPLE (315, 100, 100); 100 | const HSVColor BLOOD_RED (0, 100, 100); 101 | 102 | Adafruit_NeoPixel ring(NEOPIXEL_RING_SIZE, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); 103 | 104 | /** 105 | Maps Gerrit user IDs to lamp colors, adapt accordingly. 106 | @param userId Gerrit user ID 107 | @return The HSV color to match the specific user 108 | */ 109 | HSVColor toColor(const String& color) { 110 | if (color == "yellow") { 111 | return MELLOW_YELLOW; 112 | } else if (color == "green") { 113 | return ALIEN_GREEN; 114 | } else if (color == "purple") { 115 | return GOTH_PURPLE; 116 | } else if (color == "orange") { 117 | return KINDA_ORANGE; 118 | } else if (color == "red") { 119 | return BLOOD_RED; 120 | } else if (color == "blue") { 121 | return GREEK_BLUE; 122 | } else { 123 | return ALMOST_WHITE; 124 | } 125 | } 126 | 127 | /** 128 | Display an error pattern on the neopixels 129 | @param neopixels The neopixels to display the error 130 | */ 131 | void indicateError(Adafruit_NeoPixel& neopixels) { 132 | // Blink red LEDs sequentially to indicate an error 133 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) { 134 | neopixels.setPixelColor(pixel, 200, 0, 0); 135 | neopixels.show(); 136 | delay(ERROR_BLINK_INTERVAL); 137 | neopixels.setPixelColor(pixel, 0, 0, 0); 138 | neopixels.show(); 139 | delay(ERROR_BLINK_INTERVAL); 140 | } 141 | } 142 | 143 | /** 144 | (Re)connects the module to WiFi 145 | */ 146 | void connectToWifi() { 147 | // Set WiFi to station mode & disconnect from an AP if previously connected 148 | WiFi.mode(WIFI_STA); 149 | WiFi.disconnect(); 150 | delay(RECONNECT_TIMEOUT); 151 | 152 | // Try to connect to the internet 153 | WiFi.begin(INTERNET_SSID, PASSWORD); 154 | auto attemptsLeft = CONNECTION_RETRIES; 155 | Serial.print("Connecting"); 156 | while ((WiFi.status() != WL_CONNECTED) && (--attemptsLeft > 0)) { 157 | delay(RETRY_CONNECTION_INTERVAL); // Wait a bit before retrying 158 | Serial.print("."); 159 | } 160 | 161 | if (attemptsLeft <= 0) { 162 | Serial.println(" Connection error!"); 163 | indicateError(ring); 164 | } else { 165 | Serial.println(" Connection success"); 166 | } 167 | } 168 | 169 | /** 170 | Get all open reviews that our gerrit user has been assigned to, 171 | then figure out whether enough developers have code-reviewed 172 | each review. If this has not happened, return a color 173 | (mapped to a developer) for every un-reviewed review. 174 | Once a review has been reviewed adequately, remove ourselves 175 | from it. 176 | 177 | @return HSV colors for every unfinished code review 178 | */ 179 | std::vector getColorsForUnfinishedReviews() { 180 | HTTPClient http; 181 | http.begin(REQUEST_URL); 182 | auto httpCode = http.GET(); 183 | 184 | if (httpCode < 0 || httpCode != HTTP_CODE_OK) { 185 | Serial.printf("[%s] GET failed with code '%s'\r\n", __FUNCTION__, http.errorToString(httpCode).c_str()); 186 | http.end(); 187 | return {}; 188 | } 189 | delay(WAIT_FOR_GITHUB_RESPONSE); 190 | 191 | auto documentLength = http.getSize(); 192 | auto stream = http.getStream(); 193 | 194 | std::vector colors; 195 | if (http.connected() && (documentLength > 0 || documentLength == -1)) { 196 | while (stream.available()) { 197 | String color = stream.readStringUntil(','); 198 | colors.push_back(toColor(color)); 199 | } 200 | } 201 | 202 | if (colors.empty()) { 203 | Serial.println("Warning - Colors not found!"); 204 | } 205 | http.end(); 206 | 207 | return colors; 208 | } 209 | 210 | /** 211 | Set the specified RGB color to all the pixels 212 | 213 | @param neopixels The neopixel structure to set color 214 | @param rgbColor The RGB color to set the pixels 215 | */ 216 | 217 | void setAllPixelColor(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 218 | switch (MY_EFFECT) { 219 | case Effect::RADAR: 220 | setRadarEffect(neopixels, rgbColor); 221 | break; 222 | case Effect::PULSE: 223 | setPulseEffect(neopixels, rgbColor); 224 | break; 225 | case Effect::COOL_RADAR: 226 | setCoolRadarEffect(neopixels, rgbColor); 227 | break; 228 | default: 229 | setRadarEffect(neopixels, rgbColor); 230 | break; 231 | } 232 | } 233 | 234 | /** 235 | Perform some radar effect 236 | @param neopixels The neopixel structure to set color 237 | @param rgbColor The RGB color to set the pixels 238 | */ 239 | void setRadarEffect(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 240 | const auto pixels = neopixels.numPixels(); 241 | static const auto SLICES = 3; 242 | static uint8_t startingPixel = 0; 243 | 244 | startingPixel++; // Does not matter if it rotates back to 0 245 | 246 | // Slice the lamp in parts where the first and brightest one is our radar effect 247 | // while the rest have a progressively dimmer color. 248 | for (auto slice = 0; slice < SLICES; slice++) { 249 | for (auto pixel = slice * pixels / SLICES + startingPixel; pixel < (slice + 1) * pixels / SLICES + startingPixel; pixel++) { 250 | neopixels.setPixelColor(pixel % pixels, rgbColor.red, rgbColor.green, rgbColor.blue); 251 | } 252 | 253 | rgbColor.red /= 2; 254 | rgbColor.green /= 2; 255 | rgbColor.blue /= 2; 256 | } 257 | } 258 | 259 | /** 260 | Perform some cool radar effect 261 | @param neopixels The neopixel structure to set color 262 | @param rgbColor The RGB color to set the pixels 263 | */ 264 | void setCoolRadarEffect(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 265 | const auto pixels = neopixels.numPixels(); 266 | static uint8_t startingPixel = 0; 267 | 268 | startingPixel++; // Does not matter if it rotates back to 0 269 | 270 | for (auto pixel = 0 + startingPixel; pixel < 3 * pixels / 5 + startingPixel; pixel++) { 271 | neopixels.setPixelColor(pixel % pixels, rgbColor.red, rgbColor.green, rgbColor.blue); 272 | } 273 | 274 | RGBColor rgb1; 275 | rgb1.red = rgbColor.green; 276 | rgb1.green = rgbColor.blue; 277 | rgb1.blue = rgbColor.red; 278 | 279 | for (auto pixel = 3 * pixels / 5 + startingPixel; pixel < 4 * pixels / 5 + startingPixel; pixel++) { 280 | neopixels.setPixelColor(pixel % pixels, rgb1.red, rgb1.green, rgb1.blue); 281 | } 282 | 283 | RGBColor rgb2; 284 | rgb2.red = rgbColor.blue; 285 | rgb2.green = rgbColor.red; 286 | rgb2.blue = rgbColor.green; 287 | 288 | for (auto pixel = 4 * pixels / 5 + startingPixel; pixel < pixels + startingPixel; pixel++) { 289 | neopixels.setPixelColor(pixel % pixels, rgb2.red, rgb2.green, rgb2.blue); 290 | } 291 | } 292 | 293 | /** 294 | Perform some pulse effect 295 | @param neopixels The neopixel structure to set color 296 | @param rgbColor The RGB color to set the pixels 297 | */ 298 | void setPulseEffect(Adafruit_NeoPixel& neopixels, RGBColor& rgbColor) { 299 | for (auto pixel = 0; pixel < neopixels.numPixels(); pixel++) { 300 | neopixels.setPixelColor(pixel, rgbColor.red, rgbColor.green, rgbColor.blue); 301 | } 302 | } 303 | 304 | /** 305 | Dim for all the supplied colors throughout the specified window 306 | @param neopixels The neopixel structure to dim 307 | @param hsvColors The HSV colors to be dimmed 308 | */ 309 | void dimWithColors(Adafruit_NeoPixel& neopixels, std::vector& hsvColors) { 310 | if (hsvColors.empty()) { 311 | Serial.println("All code is reviewed, good job"); 312 | return; 313 | } 314 | // Dim every color within the designated time window 315 | // The effect we are after is the more unfinished reviews 316 | // the faster the neopixels will dim 317 | auto timeSlotForEachColor = DIM_WINDOW / hsvColors.size(); 318 | for (const auto& hsvColor : hsvColors) { 319 | auto rgb = hsvColor.toRGB(); 320 | // The time slot has to be evenly divided among the intervals necessary 321 | // to dim it all the way up and down 322 | auto dimInterval = (timeSlotForEachColor / hsvColor.value) / 2; 323 | // Dim up every pixel for the current color 324 | for (auto intensity = 0; intensity < hsvColor.value; intensity++) { 325 | // Get the RGB value of the currently dimmed HSV color 326 | auto rgbColor = HSVColor(hsvColor.hue, hsvColor.saturation, intensity).toRGB(); 327 | setAllPixelColor(neopixels, rgbColor); 328 | neopixels.show(); 329 | delay(dimInterval); 330 | } 331 | 332 | // Dim down every pixel for the current color 333 | for (auto intensity = hsvColor.value; intensity >= 0; intensity--) { 334 | // Get the RGB value of the currently dimmed HSV color 335 | auto rgbColor = HSVColor(hsvColor.hue, hsvColor.saturation, intensity).toRGB(); 336 | setAllPixelColor(neopixels, rgbColor); 337 | neopixels.show(); 338 | delay(dimInterval); 339 | } 340 | } 341 | } 342 | 343 | void setup() { 344 | Serial.begin(9600); 345 | ring.begin(); 346 | ring.show(); // Initialize all pixels to 'off' 347 | connectToWifi(); 348 | } 349 | 350 | void loop() { 351 | if (WiFi.status() != WL_CONNECTED) { 352 | connectToWifi(); 353 | } else { 354 | auto hsvColors = getColorsForUnfinishedReviews(); 355 | dimWithColors(ring, hsvColors); 356 | } 357 | delay(CHECK_FOR_REVIEWS_INTERVAL); 358 | } 359 | -------------------------------------------------------------------------------- /hardware/gerrit_lamp/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | # Except the following: 4 | !.gitignore 5 | !gerrit_lamp.brd 6 | !gerrit_lamp.sch 7 | !README.md 8 | --------------------------------------------------------------------------------