├── .github └── workflows │ └── build.yml ├── .gitignore ├── DeviceWheel ├── DeviceWheel-circuit.fzz ├── DeviceWheel-circuit.png ├── DeviceWheel-icon.svg ├── DeviceWheel_arduino │ ├── DeviceWheel_arduino.ino │ └── data │ │ ├── bootstrap-4.4.1.min.css │ │ ├── bootstrap-4.4.1.min.js │ │ ├── icon.svg │ │ ├── index.html │ │ ├── jquery-3.5.1.min.js │ │ ├── script.js │ │ └── style.css └── README.md ├── LICENSE.txt ├── README.md ├── SignalStrength ├── README.md ├── SignalStrength-circuit.fzz ├── SignalStrength-circuit.png ├── SignalStrength-icon.svg ├── SignalStrength.fzz └── SignalStrength_arduino │ ├── SignalStrength_arduino.ino │ └── data │ ├── bootstrap-4.4.1.min.css │ ├── bootstrap-4.4.1.min.js │ ├── icon.svg │ ├── index.html │ ├── jquery-3.5.1.min.js │ ├── script.js │ └── style.css ├── TrafficMonitor ├── README.md ├── TrafficMonitor-circuit.fzz ├── TrafficMonitor-circuit.png ├── TrafficMonitor-display.gif ├── TrafficMonitor-icon.svg ├── TrafficMonitor_arduino │ ├── TrafficMonitor_arduino.ino │ └── data │ │ ├── bootstrap-4.4.1.min.css │ │ ├── bootstrap-4.4.1.min.js │ │ ├── icon.svg │ │ ├── index.html │ │ ├── jquery-3.5.1.min.js │ │ ├── script.js │ │ └── style.css └── TrafficMonitor_processing │ ├── Device.pde │ ├── Observation.pde │ ├── Observations.pde │ ├── TrafficMonitor_processing.pde │ └── data │ ├── make-mac-prefixes.pl │ └── nmap-mac-prefixes └── images ├── hero-sm.gif ├── hero.gif └── hero.png /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: build 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the $default-branch branch 8 | push: 9 | branches: [ $default-branch ] 10 | pull_request: 11 | branches: [ $default-branch ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v3 27 | 28 | - name: DeviceWheel 29 | run: echo Build DeviceWheel 30 | 31 | - name: wget 32 | run: wget -O oui.txt https://standards-oui.ieee.org/oui/oui.txt 33 | 34 | - name: Set up perl 35 | uses: shogo82148/actions-setup-perl@v1 36 | with: 37 | perl-version: ${{ matrix.perl }} 38 | - run: perl -V 39 | 40 | - name: SignalStrength 41 | run: echo Build SignalStrength 42 | 43 | - name: TrafficMonitor 44 | run: echo Build TrafficMonitor 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | application*/ 4 | *.zip -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel-circuit.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/DeviceWheel/DeviceWheel-circuit.fzz -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel-circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/DeviceWheel/DeviceWheel-circuit.png -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel_arduino/DeviceWheel_arduino.ino: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Three WiFi Meters - Device Wheel 4 | - 5 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 6 | MIT License - Copyright (c) March 2021 7 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-device-wheel 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | YoYoWiFiManager wifiManager; 15 | YoYoSettings *settings; 16 | 17 | Approximate approx; 18 | 19 | #if defined(ESP8266) 20 | const int ledPin = 2; 21 | #elif defined(ESP32) 22 | const int ledPin = 14; 23 | #endif 24 | 25 | const int motorPinA = SDA; 26 | const int motorPinB = SCL; 27 | #if defined(ESP32) 28 | const int motorChannelA = 0; 29 | const int motorChannelB = 1; 30 | #endif 31 | 32 | int targetMotorSpeed = 0; 33 | const unsigned int motorUpdateIntervalMs = 250; 34 | long nextMotorUpdateAtMs = 0; 35 | 36 | bool newPair = false; 37 | 38 | void setup() { 39 | Serial.begin(115200); 40 | 41 | pinMode(ledPin, OUTPUT); 42 | digitalWrite(ledPin, LOW); 43 | 44 | pinMode(motorPinA, OUTPUT); 45 | pinMode(motorPinB, OUTPUT); 46 | #if defined(ESP32) 47 | ledcSetup(motorChannelA, 1000, 8); 48 | ledcSetup(motorChannelB, 1000, 8); 49 | ledcAttachPin(motorPinA, motorChannelA); 50 | ledcAttachPin(motorPinB, motorChannelB); 51 | #endif 52 | 53 | setMotorSpeed(1024); 54 | delay(25); 55 | setMotorSpeed(0); 56 | 57 | settings = new YoYoSettings(512); //Settings must be created here in Setup() as contains call to EEPROM.begin() which will otherwise fail 58 | wifiManager.init(settings, onceConnected, NULL, NULL, false, 80, -1); 59 | 60 | // const char *macAddress = (*settings)["pair"]; 61 | // if(macAddress) setPair(macAddress); 62 | 63 | //Attempt to connect to a WiFi network previously saved in the settings, 64 | //if one can not be found start a captive portal called "YoYoMachines", 65 | //with a password of "blinkblink" to configure a new one: 66 | wifiManager.begin("Home Network Study", "blinkblink"); 67 | } 68 | 69 | void onceConnected() { 70 | wifiManager.end(); 71 | 72 | if (approx.init("","")) { 73 | approx.setProximateDeviceHandler(onProximateDevice, APPROXIMATE_INTIMATE_RSSI, /*lastSeenTimeoutMs*/ 3000); 74 | approx.setActiveDeviceHandler(onActiveDevice, /*inclusive*/ false); 75 | approx.begin(); 76 | } 77 | } 78 | 79 | void loop() { 80 | uint8_t wifiStatus = wifiManager.loop(); 81 | approx.loop(); 82 | 83 | if(approx.isRunning()) { 84 | if(!newPair) digitalWrite(ledPin, HIGH); 85 | else { 86 | digitalWrite(ledPin, LOW); 87 | delay(200); 88 | digitalWrite(ledPin, HIGH); 89 | newPair = false; 90 | } 91 | } 92 | else { 93 | switch(wifiManager.currentMode) { 94 | case YoYoWiFiManager::YY_MODE_PEER_CLIENT: 95 | digitalWrite(ledPin, blink(1000)); 96 | break; 97 | default: //YY_MODE_PEER_SERVER 98 | digitalWrite(ledPin, blink(500)); 99 | break; 100 | } 101 | } 102 | 103 | updateMotorSpeed(); 104 | } 105 | 106 | bool blink(int periodMs) { 107 | return(((millis() / periodMs) % 2) == 0); 108 | } 109 | 110 | void onProximateDevice(Device *device, Approximate::DeviceEvent event) { 111 | switch (event) { 112 | case Approximate::ARRIVE: 113 | newPair = true; 114 | char macAdddress[18]; 115 | device -> getMacAddressAs_c_str(macAdddress); 116 | (*settings)["pair"] = macAdddress; 117 | settings->save(); 118 | setPair(macAdddress); 119 | break; 120 | case Approximate::DEPART: 121 | break; 122 | } 123 | } 124 | 125 | void setPair(const char *macAddress) { 126 | Serial.printf("Paired with: %s\n", macAddress); 127 | approx.setActiveDeviceFilter(macAddress); 128 | } 129 | 130 | void onActiveDevice(Device *device, Approximate::DeviceEvent event) { 131 | int payloadSizeByte = device -> getPayloadSizeBytes(); 132 | if(device -> isDownloading()) payloadSizeByte *= -1; 133 | 134 | setTargetMotorSpeed(payloadSizeByte); 135 | } 136 | 137 | void updateMotorSpeed() { 138 | if(millis() > nextMotorUpdateAtMs) { 139 | setMotorSpeed(targetMotorSpeed); 140 | 141 | targetMotorSpeed = 0; 142 | nextMotorUpdateAtMs = millis() + motorUpdateIntervalMs; 143 | } 144 | } 145 | 146 | void setTargetMotorSpeed(int v) { 147 | targetMotorSpeed = constrain(v, -1024, 1024); 148 | } 149 | 150 | void setMotorSpeed(int v) { 151 | v = constrain(v, -1024, 1024); 152 | 153 | float valueA = 0; 154 | float valueB = 0; 155 | 156 | if(v != 0) { 157 | if(v > 0) { 158 | valueA = v; 159 | valueB = 0; 160 | 161 | digitalWrite(motorPinA, HIGH); 162 | digitalWrite(motorPinB, LOW); 163 | } 164 | else { 165 | valueA = 0; 166 | valueB = abs(v); 167 | 168 | digitalWrite(motorPinA, LOW); 169 | digitalWrite(motorPinB, HIGH); 170 | } 171 | delay(25); 172 | } 173 | 174 | #if defined(ESP32) 175 | ledcWrite(motorChannelA, valueA); 176 | ledcWrite(motorChannelB, valueB); 177 | #else 178 | analogWrite(motorPinA, valueA); 179 | analogWrite(motorPinB, valueB); 180 | #endif 181 | } 182 | -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel_arduino/data/bootstrap-4.4.1.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Y.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Se,popperConfig:null},Fe="show",Ue="out",We={HIDE:"hide"+Oe,HIDDEN:"hidden"+Oe,SHOW:"show"+Oe,SHOWN:"shown"+Oe,INSERTED:"inserted"+Oe,CLICK:"click"+Oe,FOCUSIN:"focusin"+Oe,FOCUSOUT:"focusout"+Oe,MOUSEENTER:"mouseenter"+Oe,MOUSELEAVE:"mouseleave"+Oe},qe="fade",Me="show",Ke=".tooltip-inner",Qe=".arrow",Be="hover",Ve="focus",Ye="click",ze="manual",Xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Me))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(qe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,this._getPopperConfig(a)),g(o).addClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ue&&e._leave(null,e)};if(g(this.tip).hasClass(qe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){function e(){n._hoverState!==Fe&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),g(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()}var n=this,i=this.getTipElement(),o=g.Event(this.constructor.Event.HIDE);if(g(this.element).trigger(o),!o.isDefaultPrevented()){if(g(i).removeClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ye]=!1,this._activeTrigger[Ve]=!1,this._activeTrigger[Be]=!1,g(this.tip).hasClass(qe)){var r=_.getTransitionDurationFromElement(i);g(i).one(_.TRANSITION_END,e).emulateTransitionEnd(r)}else e();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Pe+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ke)),this.getTitle()),g(t).removeClass(qe+" "+Me)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=we(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},t._getPopperConfig=function(t){var e=this;return l({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Qe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},{},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,{},e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Re[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==ze){var e=t===Be?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Be?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),this._hideModalHandler=function(){i.element&&i.hide()},g(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");!this.element.getAttribute("title")&&"string"==t||(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ve:Be]=!0),g(e.getTipElement()).hasClass(Me)||e._hoverState===Fe?e._hoverState=Fe:(clearTimeout(e._timeout),e._hoverState=Fe,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Fe&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ve:Be]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ue,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ue&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==je.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,{},e,{},"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Le);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(qe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ne),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ne,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return xe}},{key:"NAME",get:function(){return Ae}},{key:"DATA_KEY",get:function(){return Ne}},{key:"Event",get:function(){return We}},{key:"EVENT_KEY",get:function(){return Oe}},{key:"DefaultType",get:function(){return He}}]),i}();g.fn[Ae]=Xe._jQueryInterface,g.fn[Ae].Constructor=Xe,g.fn[Ae].noConflict=function(){return g.fn[Ae]=ke,Xe._jQueryInterface};var $e="popover",Ge="bs.popover",Je="."+Ge,Ze=g.fn[$e],tn="bs-popover",en=new RegExp("(^|\\s)"+tn+"\\S+","g"),nn=l({},Xe.Default,{placement:"right",trigger:"click",content:"",template:''}),on=l({},Xe.DefaultType,{content:"(string|element|function)"}),rn="fade",sn="show",an=".popover-header",ln=".popover-body",cn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,INSERTED:"inserted"+Je,CLICK:"click"+Je,FOCUSIN:"focusin"+Je,FOCUSOUT:"focusout"+Je,MOUSEENTER:"mouseenter"+Je,MOUSELEAVE:"mouseleave"+Je},hn=function(t){function i(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}(i,t);var e=i.prototype;return e.isWithContent=function(){return this.getTitle()||this._getContent()},e.addAttachmentClass=function(t){g(this.getTipElement()).addClass(tn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},e.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(an),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ln),e),t.removeClass(rn+" "+sn)},e._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},e._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(en);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel_arduino/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |

The blue LED will stay lit if the network has been configured correctly.

38 |

If it continues to flash please reconnect to the network and try again.

39 |
40 |

David Chatting 2021

41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel_arduino/data/script.js: -------------------------------------------------------------------------------- 1 | var maxWifiNetworks = 5; 2 | var peers = []; 3 | 4 | function init() { 5 | $('#config').hide(); 6 | $('#nextstep').hide(); 7 | $('#alert-text').hide(); 8 | 9 | $('#save-button').click(onSaveButtonClicked); 10 | $('#password').keypress(onKeyPressed); 11 | 12 | $('#networks-list-select').attr('disabled', true); 13 | $('#password').attr('disabled', true); 14 | 15 | $.getJSON('/yoyo/credentials', function (json) { 16 | $('#config').show(); 17 | configure(json); 18 | }).fail(function() { 19 | $('#alert-text').show(); 20 | $('#alert-text').addClass('alert-danger'); 21 | $('#alert-text').text('Error'); 22 | }); 23 | } 24 | 25 | function configure(json) { 26 | $('#config').show(); 27 | 28 | console.log(json); 29 | 30 | populateNetworksList(); 31 | populatePeersList(); 32 | } 33 | 34 | function onKeyPressed(event) { 35 | if (event.keyCode == 13) { 36 | onSaveButtonClicked(event); 37 | } 38 | } 39 | 40 | function populateNetworksList(selectedNetwork) { 41 | let networks = $('#networks-list-select'); 42 | 43 | $.getJSON('/yoyo/networks', function (json) { 44 | if(json.length > 0) { 45 | networks.empty(); 46 | //Order the networks by signal strength and limit to top n 47 | json = json.sort((a, b) => parseInt(b.RSSI) - parseInt(a.RSSI)); 48 | var ssidList = json.slice(0, maxWifiNetworks).map(i => { 49 | return i.SSID; 50 | }); 51 | 52 | //The selected network will always remain: 53 | if(selectedNetwork && !ssidList.includes(selectedNetwork)) ssidList.push(selectedNetwork); 54 | 55 | $.each(ssidList, function (key, entry) { 56 | let network = $(''); 57 | 58 | network.attr('value', entry).text(entry); 59 | if(entry == selectedNetwork) network.attr('selected', true); 60 | 61 | networks.append(network); 62 | }); 63 | 64 | $('#networks-list-select').attr('disabled', false); 65 | $('#password').attr('disabled', false); 66 | } 67 | 68 | if($('#networks-list-select option').length == 0) { 69 | networks.append(''); 70 | } 71 | 72 | setTimeout(function() { 73 | populateNetworksList($('#networks-list-select').children("option:selected").val()); 74 | }, 10000); 75 | }); 76 | } 77 | 78 | function populatePeersList() { 79 | $.getJSON('/yoyo/peers', function (json) { 80 | if(json.length > 0) { 81 | var newPeers = json.map(i => { return i.IP;}); 82 | newPeers.forEach(ip => { if(!peers.includes(ip)) addPeer(ip); }); 83 | peers.forEach(ip => { if(!newPeers.includes(ip)) removePeer(ip); }); 84 | 85 | peers = newPeers; 86 | } 87 | }); 88 | 89 | setTimeout(function() { 90 | populatePeersList(); 91 | }, 15000); 92 | } 93 | 94 | function addPeer(ip) { 95 | console.log("addPeer > " + ip); 96 | 97 | let peersListDiv = $('#peers-list'); 98 | 99 | let span = $(''); 100 | span.attr('id', ip); 101 | 102 | let image = $(''); 103 | image.attr('class', 'peer-image'); 104 | image.attr('src', "http://" + ip + "/icon.svg"); 105 | 106 | peersListDiv.append(span); 107 | span.append(image); 108 | } 109 | 110 | function removePeer(ip) { 111 | console.log("removePeer > " + ip); 112 | 113 | $('span[id="' + ip + '"]').remove(); 114 | } 115 | 116 | function onSaveButtonClicked(event) { 117 | event.preventDefault(); 118 | 119 | var data = { 120 | ssid: $('#networks-list-select').children("option:selected").val(), 121 | password: $('#password').val() 122 | }; 123 | 124 | var request = { 125 | type: "POST", 126 | url: "/yoyo/credentials", 127 | data: JSON.stringify(data), 128 | dataType: 'json', 129 | contentType: 'application/json; charset=utf-8', 130 | cache: false, 131 | timeout: 15000, 132 | async: false, 133 | success: function(response, textStatus, jqXHR) { 134 | console.log(response); 135 | $('#config').hide(); 136 | $('#alert-text').show(); 137 | $('#alert-text').removeClass('alert-danger'); 138 | $('#alert-text').addClass('alert-success'); 139 | $('#alert-text').text('Saved'); 140 | $('#nextstep').show(); 141 | }, 142 | error: function (jqXHR, textStatus, errorThrown) { 143 | console.log(jqXHR); 144 | console.log(textStatus); 145 | console.log(errorThrown); 146 | $('#alert-text').show(); 147 | $('#alert-text').addClass('alert-danger'); 148 | $('#alert-text').text('Couldn\'t Save'); 149 | } 150 | } 151 | 152 | //json validation fails on Safari - but if defaults to text then fails on Windows/Android 153 | if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && navigator.userAgent.indexOf('Chromium') == -1) { 154 | request.dataType = 'text'; 155 | } 156 | 157 | $.ajax(request); 158 | } -------------------------------------------------------------------------------- /DeviceWheel/DeviceWheel_arduino/data/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | background-color: #f5f5f5; 4 | } 5 | 6 | canvas { 7 | display: block; 8 | } 9 | 10 | .yo-yo-form { 11 | width: 100%; 12 | max-width: 330px; 13 | padding: 15px; 14 | margin: 0 auto; 15 | } 16 | .yo-yo-form .checkbox { 17 | font-weight: 400; 18 | } 19 | .yo-yo-form .form-control { 20 | position: relative; 21 | box-sizing: border-box; 22 | padding: 10px; 23 | font-size: 16px; 24 | } 25 | .yo-yo-form .form-control:focus { 26 | z-index: 2; 27 | } 28 | .yo-yo-form input[type="email"], .yo-yo-form select { 29 | margin-bottom: -1px; 30 | border-bottom-right-radius: 0; 31 | border-bottom-left-radius: 0; 32 | } 33 | .yo-yo-form input[type="password"] { 34 | margin-bottom: 10px; 35 | border-top-left-radius: 0; 36 | border-top-right-radius: 0; 37 | } 38 | 39 | .dropdown-menu.pull-left { 40 | left: 0; 41 | } 42 | 43 | .peers { 44 | height: 130px; 45 | } 46 | 47 | .peer-image { 48 | height: 100%; 49 | padding-left: 10px; 50 | padding-right: 10px; 51 | } -------------------------------------------------------------------------------- /DeviceWheel/README.md: -------------------------------------------------------------------------------- 1 | # Device Wheel 2 | Watch an individual device's use of the network with this Device Wheel. Once the WiFi is configured by joining the "Home Network Study" network and setting the credentials via the captive portal, bringing an IoT device in proximity of the Device Wheel will cause it to pair and the wheel will spin whenever the is network activity - clockwise for downloads, anti-clockwise for uploads. 3 | 4 | ## Hardware 5 | * Adafruit HUZZAH32 – ESP32 Feather Board - https://www.adafruit.com/product/3405 6 | * 3800 RPM 1.5mm Diameter Shaft 2V DC Motor for Walkman 7 | * 1.5A Mini Speed Control Dual Channel Motor Driver MX1508 8 | * SPDT Mini Power Switch - https://shop.pimoroni.com/products/spdt-mini-power-switch 9 | * 3mm blue LED 10 | * 1K Ohm resistor 11 | * LiPo Battery Pack 3.7V 500mAh - https://shop.pimoroni.com/products/lipo-battery-pack?variant=20429082055 12 | * Micro USB male to Micro USB female charge + data adapter cable 13 | 14 | 15 | 16 | The circuit shows an Adafruit HUZZAH32, but the code will compile for any ESP8266 or ESP32 (pin assignments will need to change of course). 17 | 18 | ## Software 19 | ### Arduino 20 | The Arduino core for the ESP8266 or ESP32 must be installed for the Arduino IDE: 21 | * ESP8266 - https://github.com/esp8266/Arduino#installing-with-boards-manager 22 | * ESP32 - https://github.com/espressif/arduino-esp32/blob/master/docs/arduino-ide/boards_manager.md 23 | 24 | And the Sketch Data Folder Uploader Tool: 25 | * ESP8266 - https://randomnerdtutorials.com/install-esp8266-filesystem-uploader-arduino-ide/ 26 | * ESP32 - https://randomnerdtutorials.com/install-esp32-filesystem-uploader-arduino-ide/ 27 | 28 | And following Arduino libraries are required: 29 | * Approximate - https://github.com/davidchatting/Approximate/ 30 | * YoYoWiFiManager - https://github.com/interactionresearchstudio/YoYoWiFiManager 31 | * ListLib - https://github.com/luisllamasbinaburo/Arduino-List 32 | 33 | From the *Tools* menu then select either `Generic ESP822 Module` or `ESP32 Dev Module` and then for the ESP8266 select `4MB (FS:3MB OTA:~512KB)` for *Flash Size* and for the ESP32 select a *Partition Scheme* of `Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)` - assuming a *Flash Size* of 4MB. Then upload the associated `data` folder using the uploader tool - also found under the *Tools* menu. The data folder contains the HTML, JavaScript and image files for the captive portal that configures the WiFi. If you don't upload the data folder the portal will say, *Yo Yo Machines default HTML*. 34 | 35 | ## WiFi Set-up 36 | The WiFi is configured by joining the *Home Network Study* network with the password *blinkblink* and entering the details of your network. If multiple meters are started once they will automaically discover each other and the set-up will shared between them. This is enabled by the [YoYoWiFiManager](https://github.com/interactionresearchstudio/YoYoWiFiManager). 37 | 38 | ## Author 39 | The Three WiFi Meters were created by David Chatting ([@davidchatting](https://twitter.com/davidchatting)) as part of the [A Network of One's Own](http://davidchatting.com/nooo/) project. This code is licensed under the [MIT License](LICENSE.txt). -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Chatting - github.com/davidchatting/ThreeWiFiMeters 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 | # Three WiFi Meters 2 | 3 | 4 | 5 | The Three WiFi Meters are three ways of experiencing WiFi networks, each attempts to disclose properties of this near ubiquitous technology. They are built for the [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32) using the [Approximate](https://github.com/davidchatting/Approximate) and [YoYoWiFiManager](https://github.com/interactionresearchstudio/YoYoWiFiManager) Arduino libraries. 6 | 7 | ## [ Signal Strength](SignalStrength) 8 | Measure the signal strength of your router around the house with the [Signal Strength Meter](SignalStrength). 9 | 10 | ## [ Device Wheel](DeviceWheel) 11 | Watch an individual device's use of the network with the [Device Wheel](DeviceWheel). 12 | 13 | ## [ Traffic Monitor](TrafficMonitor) 14 | See the traffic on your home WiFi network with the [Traffic Monitor](TrafficMonitor). 15 | 16 | ## Limitations 17 | The meters work with 2.4GHz WiFi networks, but not 5GHz networks - as neither ESP8266 or ESP32 support this technology. 18 | 19 | ## A Little History 20 | The Three WiFi Meters are inspired by multiple sources, not least Natalie Jeremijenko's Live Wire (also known as Dangling String) [(Weiser and Brown, 1995)](https://web.archive.org/web/19970624041814/http://www.powergrid.com/1.01/calmtech.html) and the Tangible Media Group's Pinwheels [(Dahley, Wisneski and Ishii, 1998)](https://tangible.media.mit.edu/project/pinwheels/). 21 | 22 | ## Author 23 | The Three WiFi Meters were created by David Chatting ([@davidchatting](https://twitter.com/davidchatting)) as part of the [A Network of One's Own](http://davidchatting.com/nooo/) project. This code is licensed under the [MIT License](LICENSE.txt). -------------------------------------------------------------------------------- /SignalStrength/README.md: -------------------------------------------------------------------------------- 1 | # Signal Strength 2 | Measure the signal strength of your router around the house with this Signal Strength Meter. Once the WiFi is configured by joining the "Home Network Study" network and setting the credentials via the captive portal, the analogue meter will reflect the signal strength (RSSI) of the router at that location. 3 | 4 | ## Hardware 5 | * Adafruit HUZZAH32 – ESP32 Feather Board - https://www.adafruit.com/product/3405 6 | * Eisco 0-30V Single Range Moving Coil Voltmeter - https://www.rapidonline.com/eisco-0-30v-single-range-moving-coil-voltmeter-52-3502 (series resistor needs to be removed) 7 | * SPDT Mini Power Switch - https://shop.pimoroni.com/products/spdt-mini-power-switch 8 | * 3mm blue LED 9 | * 1K Ohm resistor 10 | * LiPo Battery Pack 3.7V 500mAh - https://shop.pimoroni.com/products/lipo-battery-pack?variant=20429082055 11 | * Micro USB male to Micro USB female charge + data adapter cable 12 | 13 | 14 | 15 | The circuit shows an Adafruit HUZZAH32, but the code will compile for any ESP8266 or ESP32 (pin assignments will need to change of course). 16 | 17 | ## Software 18 | ### Arduino 19 | The Arduino core for the ESP8266 or ESP32 must be installed for the Arduino IDE: 20 | * ESP8266 - https://github.com/esp8266/Arduino#installing-with-boards-manager 21 | * ESP32 - https://github.com/espressif/arduino-esp32/blob/master/docs/arduino-ide/boards_manager.md 22 | 23 | And the Sketch Data Folder Uploader Tool: 24 | * ESP8266 - https://randomnerdtutorials.com/install-esp8266-filesystem-uploader-arduino-ide/ 25 | * ESP32 - https://randomnerdtutorials.com/install-esp32-filesystem-uploader-arduino-ide/ 26 | 27 | And following Arduino libraries are required: 28 | * Approximate - https://github.com/davidchatting/Approximate/ 29 | * YoYoWiFiManager - https://github.com/interactionresearchstudio/YoYoWiFiManager 30 | * ListLib - https://github.com/luisllamasbinaburo/Arduino-List 31 | 32 | From the *Tools* menu then select either `Generic ESP822 Module` or `ESP32 Dev Module` and then for the ESP8266 select `4MB (FS:3MB OTA:~512KB)` for *Flash Size* and for the ESP32 select a *Partition Scheme* of `Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)` - assuming a *Flash Size* of 4MB. Then upload the associated `data` folder using the uploader tool - also found under the *Tools* menu. The data folder contains the HTML, JavaScript and image files for the captive portal that configures the WiFi. If you don't upload the data folder the portal will say, *Yo Yo Machines default HTML*. 33 | 34 | ## WiFi Set-up 35 | The WiFi is configured by joining the *Home Network Study* network with the password *blinkblink* and entering the details of your network. If multiple meters are started once they will automaically discover each other and the set-up will shared between them. This is enabled by the [YoYoWiFiManager](https://github.com/interactionresearchstudio/YoYoWiFiManager). 36 | 37 | ## Author 38 | The Three WiFi Meters were created by David Chatting ([@davidchatting](https://twitter.com/davidchatting)) as part of the [A Network of One's Own](http://davidchatting.com/nooo/) project. This code is licensed under the [MIT License](LICENSE.txt). -------------------------------------------------------------------------------- /SignalStrength/SignalStrength-circuit.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/SignalStrength/SignalStrength-circuit.fzz -------------------------------------------------------------------------------- /SignalStrength/SignalStrength-circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/SignalStrength/SignalStrength-circuit.png -------------------------------------------------------------------------------- /SignalStrength/SignalStrength-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SignalStrength/SignalStrength.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/SignalStrength/SignalStrength.fzz -------------------------------------------------------------------------------- /SignalStrength/SignalStrength_arduino/SignalStrength_arduino.ino: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Three WiFi Meters - Signal Strength 4 | - 5 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 6 | MIT License - Copyright (c) March 2021 7 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-signal-strength 8 | */ 9 | 10 | #include 11 | #include 12 | 13 | YoYoWiFiManager wifiManager; 14 | YoYoSettings *settings; 15 | 16 | const int ledPin = 12; 17 | const int gaguePin = 5; 18 | const int maxGagueValue = 255; 19 | 20 | #if defined(ESP32) 21 | const int gagueChannel = 0; 22 | #endif 23 | 24 | const int minRSSI = -80; 25 | const int maxRSSI = -20; 26 | 27 | void setup() { 28 | Serial.begin(115200); 29 | 30 | pinMode(ledPin, OUTPUT); 31 | digitalWrite(ledPin, LOW); 32 | 33 | pinMode(gaguePin, OUTPUT); 34 | #if defined(ESP32) 35 | ledcSetup(gagueChannel, 1000, 8); 36 | ledcAttachPin(gaguePin, gagueChannel); 37 | #endif 38 | 39 | setGague(255); 40 | delay(150); 41 | setGague(0); 42 | 43 | settings = new YoYoSettings(512); //Settings must be created here in Setup() as contains call to EEPROM.begin() which will otherwise fail 44 | wifiManager.init(settings, onceConnected, NULL, NULL, false, 80, -1); 45 | 46 | //Attempt to connect to a WiFi network previously saved in the settings, 47 | //if one can not be found start a captive portal called "YoYoMachines", 48 | //with a password of "blinkblink" to configure a new one: 49 | wifiManager.begin("Home Network Study", "blinkblink"); 50 | } 51 | 52 | void onceConnected() { 53 | } 54 | 55 | void loop() { 56 | uint8_t wifiStatus = wifiManager.loop(); 57 | 58 | if(wifiStatus == YY_CONNECTED) { 59 | digitalWrite(ledPin, HIGH); 60 | displayRSSI(); 61 | } 62 | else { 63 | switch(wifiManager.currentMode) { 64 | case YoYoWiFiManager::YY_MODE_PEER_CLIENT: 65 | digitalWrite(ledPin, blink(1000)); 66 | break; 67 | default: //YY_MODE_PEER_SERVER 68 | digitalWrite(ledPin, blink(500)); 69 | break; 70 | } 71 | setGague(0); 72 | } 73 | } 74 | 75 | bool blink(int periodMs) { 76 | return(((millis() / periodMs) % 2) == 0); 77 | } 78 | 79 | void displayRSSI() { 80 | int32_t rssi = getRSSI(WiFi.SSID()); 81 | if(rssi == 0){ 82 | setGague(0); 83 | } 84 | else{ 85 | int valueToDisplay = map(rssi, maxRSSI, minRSSI, 255, 0); 86 | valueToDisplay = min(max(valueToDisplay, 0), 255); 87 | setGague(valueToDisplay); 88 | } 89 | } 90 | 91 | void setGague(int value) { 92 | value = map(value, 0, 255, 0, maxGagueValue); 93 | 94 | #if defined(ESP32) 95 | ledcWrite(gagueChannel, value); 96 | #else 97 | analogWrite(gaguePin, value); 98 | #endif 99 | } 100 | 101 | // Return RSSI or 0 if target SSID not found 102 | int32_t getRSSI(String target_ssid) { 103 | int32_t result = 0; 104 | 105 | byte available_networks = WiFi.scanNetworks(); 106 | 107 | for (int network = 0; network < available_networks && result == 0; network++) { 108 | if (WiFi.SSID(network).equals(target_ssid)) { 109 | result = WiFi.RSSI(network); 110 | } 111 | } 112 | return(result); 113 | } 114 | -------------------------------------------------------------------------------- /SignalStrength/SignalStrength_arduino/data/bootstrap-4.4.1.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Y.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Se,popperConfig:null},Fe="show",Ue="out",We={HIDE:"hide"+Oe,HIDDEN:"hidden"+Oe,SHOW:"show"+Oe,SHOWN:"shown"+Oe,INSERTED:"inserted"+Oe,CLICK:"click"+Oe,FOCUSIN:"focusin"+Oe,FOCUSOUT:"focusout"+Oe,MOUSEENTER:"mouseenter"+Oe,MOUSELEAVE:"mouseleave"+Oe},qe="fade",Me="show",Ke=".tooltip-inner",Qe=".arrow",Be="hover",Ve="focus",Ye="click",ze="manual",Xe=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Me))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(qe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,this._getPopperConfig(a)),g(o).addClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===Ue&&e._leave(null,e)};if(g(this.tip).hasClass(qe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){function e(){n._hoverState!==Fe&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),g(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()}var n=this,i=this.getTipElement(),o=g.Event(this.constructor.Event.HIDE);if(g(this.element).trigger(o),!o.isDefaultPrevented()){if(g(i).removeClass(Me),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ye]=!1,this._activeTrigger[Ve]=!1,this._activeTrigger[Be]=!1,g(this.tip).hasClass(qe)){var r=_.getTransitionDurationFromElement(i);g(i).one(_.TRANSITION_END,e).emulateTransitionEnd(r)}else e();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Pe+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ke)),this.getTitle()),g(t).removeClass(qe+" "+Me)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=we(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},t._getPopperConfig=function(t){var e=this;return l({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Qe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},{},this.config.popperConfig)},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,{},e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Re[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==ze){var e=t===Be?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Be?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),this._hideModalHandler=function(){i.element&&i.hide()},g(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");!this.element.getAttribute("title")&&"string"==t||(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ve:Be]=!0),g(e.getTipElement()).hasClass(Me)||e._hoverState===Fe?e._hoverState=Fe:(clearTimeout(e._timeout),e._hoverState=Fe,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Fe&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ve:Be]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ue,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ue&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==je.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,{},e,{},"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(Ae,t,this.constructor.DefaultType),t.sanitize&&(t.template=we(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Le);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(qe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ne),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ne,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return xe}},{key:"NAME",get:function(){return Ae}},{key:"DATA_KEY",get:function(){return Ne}},{key:"Event",get:function(){return We}},{key:"EVENT_KEY",get:function(){return Oe}},{key:"DefaultType",get:function(){return He}}]),i}();g.fn[Ae]=Xe._jQueryInterface,g.fn[Ae].Constructor=Xe,g.fn[Ae].noConflict=function(){return g.fn[Ae]=ke,Xe._jQueryInterface};var $e="popover",Ge="bs.popover",Je="."+Ge,Ze=g.fn[$e],tn="bs-popover",en=new RegExp("(^|\\s)"+tn+"\\S+","g"),nn=l({},Xe.Default,{placement:"right",trigger:"click",content:"",template:''}),on=l({},Xe.DefaultType,{content:"(string|element|function)"}),rn="fade",sn="show",an=".popover-header",ln=".popover-body",cn={HIDE:"hide"+Je,HIDDEN:"hidden"+Je,SHOW:"show"+Je,SHOWN:"shown"+Je,INSERTED:"inserted"+Je,CLICK:"click"+Je,FOCUSIN:"focusin"+Je,FOCUSOUT:"focusout"+Je,MOUSEENTER:"mouseenter"+Je,MOUSELEAVE:"mouseleave"+Je},hn=function(t){function i(){return t.apply(this,arguments)||this}!function(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}(i,t);var e=i.prototype;return e.isWithContent=function(){return this.getTitle()||this._getContent()},e.addAttachmentClass=function(t){g(this.getTipElement()).addClass(tn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},e.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(an),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ln),e),t.removeClass(rn+" "+sn)},e._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},e._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(en);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t -------------------------------------------------------------------------------- /SignalStrength/SignalStrength_arduino/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |

The blue LED will stay lit if the network has been configured correctly.

38 |

If it continues to flash please reconnect to the network and try again.

39 |
40 |

David Chatting 2021

41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /SignalStrength/SignalStrength_arduino/data/script.js: -------------------------------------------------------------------------------- 1 | var maxWifiNetworks = 5; 2 | var peers = []; 3 | 4 | function init() { 5 | $('#config').hide(); 6 | $('#nextstep').hide(); 7 | $('#alert-text').hide(); 8 | 9 | $('#save-button').click(onSaveButtonClicked); 10 | $('#password').keypress(onKeyPressed); 11 | 12 | $('#networks-list-select').attr('disabled', true); 13 | $('#password').attr('disabled', true); 14 | 15 | $.getJSON('/yoyo/credentials', function (json) { 16 | $('#config').show(); 17 | configure(json); 18 | }).fail(function() { 19 | $('#alert-text').show(); 20 | $('#alert-text').addClass('alert-danger'); 21 | $('#alert-text').text('Error'); 22 | }); 23 | } 24 | 25 | function configure(json) { 26 | $('#config').show(); 27 | 28 | console.log(json); 29 | 30 | populateNetworksList(); 31 | populatePeersList(); 32 | } 33 | 34 | function onKeyPressed(event) { 35 | if (event.keyCode == 13) { 36 | onSaveButtonClicked(event); 37 | } 38 | } 39 | 40 | function populateNetworksList(selectedNetwork) { 41 | let networks = $('#networks-list-select'); 42 | 43 | $.getJSON('/yoyo/networks', function (json) { 44 | if(json.length > 0) { 45 | networks.empty(); 46 | //Order the networks by signal strength and limit to top n 47 | json = json.sort((a, b) => parseInt(b.RSSI) - parseInt(a.RSSI)); 48 | var ssidList = json.slice(0, maxWifiNetworks).map(i => { 49 | return i.SSID; 50 | }); 51 | 52 | //The selected network will always remain: 53 | if(selectedNetwork && !ssidList.includes(selectedNetwork)) ssidList.push(selectedNetwork); 54 | 55 | $.each(ssidList, function (key, entry) { 56 | let network = $(''); 57 | 58 | network.attr('value', entry).text(entry); 59 | if(entry == selectedNetwork) network.attr('selected', true); 60 | 61 | networks.append(network); 62 | }); 63 | 64 | $('#networks-list-select').attr('disabled', false); 65 | $('#password').attr('disabled', false); 66 | } 67 | 68 | if($('#networks-list-select option').length == 0) { 69 | networks.append(''); 70 | } 71 | 72 | setTimeout(function() { 73 | populateNetworksList($('#networks-list-select').children("option:selected").val()); 74 | }, 10000); 75 | }); 76 | } 77 | 78 | function populatePeersList() { 79 | $.getJSON('/yoyo/peers', function (json) { 80 | if(json.length > 0) { 81 | var newPeers = json.map(i => { return i.IP;}); 82 | newPeers.forEach(ip => { if(!peers.includes(ip)) addPeer(ip); }); 83 | peers.forEach(ip => { if(!newPeers.includes(ip)) removePeer(ip); }); 84 | 85 | peers = newPeers; 86 | } 87 | }); 88 | 89 | setTimeout(function() { 90 | populatePeersList(); 91 | }, 15000); 92 | } 93 | 94 | function addPeer(ip) { 95 | console.log("addPeer > " + ip); 96 | 97 | let peersListDiv = $('#peers-list'); 98 | 99 | let span = $(''); 100 | span.attr('id', ip); 101 | 102 | let image = $(''); 103 | image.attr('class', 'peer-image'); 104 | image.attr('src', "http://" + ip + "/icon.svg"); 105 | 106 | peersListDiv.append(span); 107 | span.append(image); 108 | } 109 | 110 | function removePeer(ip) { 111 | console.log("removePeer > " + ip); 112 | 113 | $('span[id="' + ip + '"]').remove(); 114 | } 115 | 116 | function onSaveButtonClicked(event) { 117 | event.preventDefault(); 118 | 119 | var data = { 120 | ssid: $('#networks-list-select').children("option:selected").val(), 121 | password: $('#password').val() 122 | }; 123 | 124 | var request = { 125 | type: "POST", 126 | url: "/yoyo/credentials", 127 | data: JSON.stringify(data), 128 | dataType: 'json', 129 | contentType: 'application/json; charset=utf-8', 130 | cache: false, 131 | timeout: 15000, 132 | async: false, 133 | success: function(response, textStatus, jqXHR) { 134 | console.log(response); 135 | $('#config').hide(); 136 | $('#alert-text').show(); 137 | $('#alert-text').removeClass('alert-danger'); 138 | $('#alert-text').addClass('alert-success'); 139 | $('#alert-text').text('Saved'); 140 | $('#nextstep').show(); 141 | }, 142 | error: function (jqXHR, textStatus, errorThrown) { 143 | console.log(jqXHR); 144 | console.log(textStatus); 145 | console.log(errorThrown); 146 | $('#alert-text').show(); 147 | $('#alert-text').addClass('alert-danger'); 148 | $('#alert-text').text('Couldn\'t Save'); 149 | } 150 | } 151 | 152 | //json validation fails on Safari - but if defaults to text then fails on Windows/Android 153 | if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && navigator.userAgent.indexOf('Chromium') == -1) { 154 | request.dataType = 'text'; 155 | } 156 | 157 | $.ajax(request); 158 | } -------------------------------------------------------------------------------- /SignalStrength/SignalStrength_arduino/data/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | background-color: #f5f5f5; 4 | } 5 | 6 | canvas { 7 | display: block; 8 | } 9 | 10 | .yo-yo-form { 11 | width: 100%; 12 | max-width: 330px; 13 | padding: 15px; 14 | margin: 0 auto; 15 | } 16 | .yo-yo-form .checkbox { 17 | font-weight: 400; 18 | } 19 | .yo-yo-form .form-control { 20 | position: relative; 21 | box-sizing: border-box; 22 | padding: 10px; 23 | font-size: 16px; 24 | } 25 | .yo-yo-form .form-control:focus { 26 | z-index: 2; 27 | } 28 | .yo-yo-form input[type="email"], .yo-yo-form select { 29 | margin-bottom: -1px; 30 | border-bottom-right-radius: 0; 31 | border-bottom-left-radius: 0; 32 | } 33 | .yo-yo-form input[type="password"] { 34 | margin-bottom: 10px; 35 | border-top-left-radius: 0; 36 | border-top-right-radius: 0; 37 | } 38 | 39 | .dropdown-menu.pull-left { 40 | left: 0; 41 | } 42 | 43 | .peers { 44 | height: 130px; 45 | } 46 | 47 | .peer-image { 48 | height: 100%; 49 | padding-left: 10px; 50 | padding-right: 10px; 51 | } -------------------------------------------------------------------------------- /TrafficMonitor/README.md: -------------------------------------------------------------------------------- 1 | # Traffic Monitor 2 | See the traffic on your home WiFi network with this Traffic Monitor. Once the WiFi is configured by joining the "Home Network Study" network and setting the credentials via the captive portal, the last three minutes of network traffic will be displayed. The devices on the network are shown on the periphery of the circle and flash when they are active. 3 | 4 | 5 | 6 | ## Hardware 7 | The ESP8266 monitors the network traffic and communicates over a serial connection with the Raspberry Pi, which renders the visualisation. 8 | 9 | * Wemos D1 mini ESP8266 10 | * Raspberry Pi Zero - https://shop.pimoroni.com/products/raspberry-pi-zero-wh-with-pre-soldered-header 11 | * Waveshare 4 inch Raspberry Pi Display - https://www.waveshare.com/wiki/4inch_RPi_LCD_(A) 12 | * 3mm blue LED 13 | * 1K Ohm resistor 14 | * Micro USB male to Micro USB female charge + data adapter cable 15 | 16 | 17 | 18 | The circuit shows a Wemos D1 mini ESP8266, but the code will compile for any ESP8266 or ESP32 (pin assignments will need to change of course). 19 | 20 | ## Software 21 | ### Arduino 22 | The Arduino core for the ESP8266 or ESP32 must be installed for the Arduino IDE: 23 | * ESP8266 - https://github.com/esp8266/Arduino#installing-with-boards-manager 24 | * ESP32 - https://github.com/espressif/arduino-esp32/blob/master/docs/arduino-ide/boards_manager.md 25 | 26 | And the Sketch Data Folder Uploader Tool: 27 | * ESP8266 - https://randomnerdtutorials.com/install-esp8266-filesystem-uploader-arduino-ide/ 28 | * ESP32 - https://randomnerdtutorials.com/install-esp32-filesystem-uploader-arduino-ide/ 29 | 30 | And following Arduino libraries are required: 31 | * Approximate - https://github.com/davidchatting/Approximate/ 32 | * YoYoWiFiManager - https://github.com/interactionresearchstudio/YoYoWiFiManager 33 | * ListLib - https://github.com/luisllamasbinaburo/Arduino-List 34 | 35 | From the *Tools* menu then select either `Generic ESP822 Module` or `ESP32 Dev Module` and then for the ESP8266 select `4MB (FS:3MB OTA:~512KB)` for *Flash Size* and for the ESP32 select a *Partition Scheme* of `Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)` - assuming a *Flash Size* of 4MB. Then upload the associated `data` folder using the uploader tool - also found under the *Tools* menu. The data folder contains the HTML, JavaScript and image files for the captive portal that configures the WiFi. If you don't upload the data folder the portal will say, *Yo Yo Machines default HTML*. 36 | 37 | ### Processing 38 | The visulisation code is built for [Processing 3](https://processing.org/) from a desktop this can be easily uploaded to the Raspberry Pi using the [Upload to Pi Tool](https://github.com/gohai/processing-uploadtopi). Alternatively, if the ESP8266 is attached to a desktop machine via USB then it can be run there instead - for platforms other than MacOS that might need some modifcation of ```looksLikeArduino()``` in the sketch. 39 | 40 | ## WiFi Set-up 41 | The WiFi is configured by joining the *Home Network Study* network with the password *blinkblink* and entering the details of your network. If multiple meters are started once they will automaically discover each other and the set-up will shared between them. This is enabled by the [YoYoWiFiManager](https://github.com/interactionresearchstudio/YoYoWiFiManager). 42 | 43 | ## Author 44 | The Three WiFi Meters were created by David Chatting ([@davidchatting](https://twitter.com/davidchatting)) as part of the [A Network of One's Own](http://davidchatting.com/nooo/) project. This code is licensed under the [MIT License](LICENSE.txt). -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor-circuit.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/TrafficMonitor/TrafficMonitor-circuit.fzz -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor-circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/TrafficMonitor/TrafficMonitor-circuit.png -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor-display.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/TrafficMonitor/TrafficMonitor-display.gif -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_arduino/TrafficMonitor_arduino.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Three WiFi Meters - Traffic Monitor 3 | - 4 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 5 | MIT License - Copyright (c) March 2021 6 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-traffic-monitor 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | YoYoWiFiManager wifiManager; 14 | YoYoSettings *settings; 15 | 16 | Approximate approx; 17 | 18 | const int ledPin = 16; //DO 19 | 20 | List activeDevices; 21 | const int maxActiveDevices = 64; 22 | const int minPayloadSizeBytes = 64; 23 | char status[32]; 24 | 25 | void setup() { 26 | Serial.begin(115200); 27 | 28 | pinMode(ledPin, OUTPUT); 29 | digitalWrite(ledPin, LOW); 30 | 31 | settings = new YoYoSettings(512); //Settings must be created here in Setup() as contains call to EEPROM.begin() which will otherwise fail 32 | wifiManager.init(settings, onceConnected, NULL, NULL, false, 80, -1); 33 | 34 | //Attempt to connect to a WiFi network previously saved in the settings, 35 | //if one can not be found start a captive portal called "YoYoMachines", 36 | //with a password of "blinkblink" to configure a new one: 37 | wifiManager.begin("Home Network Study", "blinkblink"); 38 | randomSeed(analogRead(0)); 39 | } 40 | 41 | void onceConnected() { 42 | wifiManager.end(); 43 | 44 | if (approx.init("", "", false, false, false)) { 45 | approx.setActiveDeviceHandler(onActiveDevice); 46 | approx.begin(); 47 | } 48 | } 49 | 50 | void loop() { 51 | uint8_t wifiStatus = wifiManager.loop(); 52 | approx.loop(); 53 | 54 | if(approx.isRunning()) { 55 | digitalWrite(ledPin, HIGH); 56 | } 57 | else { 58 | switch(wifiManager.currentMode) { 59 | case YoYoWiFiManager::YY_MODE_PEER_CLIENT: 60 | digitalWrite(ledPin, blink(1000)); 61 | break; 62 | default: //YY_MODE_PEER_SERVER 63 | digitalWrite(ledPin, blink(500)); 64 | break; 65 | } 66 | } 67 | 68 | if(Serial.available()) { 69 | serialEvent(); 70 | } 71 | } 72 | 73 | bool blink(int periodMs) { 74 | return(((millis() / periodMs) % 2) == 0); 75 | } 76 | 77 | void onActiveDevice(Device *device, Approximate::DeviceEvent event) { 78 | int n = activeDevices.Count(); 79 | if (n <= maxActiveDevices) { 80 | if(n == 0 || device -> getPayloadSizeBytes() > minPayloadSizeBytes || random(10) == 0) { 81 | activeDevices.Add(new Device(device)); 82 | } 83 | } 84 | } 85 | 86 | void serialEvent() { 87 | while (Serial.available()) { 88 | if((char)Serial.read() == 'x') { 89 | wifiManager.getStatusAsString(status); 90 | 91 | if(activeDevices.Count() > 0) { 92 | char macAddress[18]; 93 | 94 | while (activeDevices.Count() > 0) { 95 | Device *activeDevice = activeDevices[0]; 96 | 97 | Serial.printf("[aprx]\t%s\t%s\t%i\t%i\n", status, activeDevice->getMacAddressAs_c_str(macAddress), activeDevice->getUploadSizeBytes(), activeDevice->getDownloadSizeBytes()); 98 | activeDevices.Remove(0); 99 | delete activeDevice; 100 | } 101 | } 102 | else { 103 | Serial.printf("[aprx]\t%s\n", status); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_arduino/data/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_arduino/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |

The blue LED will stay lit if the network has been configured correctly.

38 |

If it continues to flash please reconnect to the network and try again.

39 |
40 |

David Chatting 2021

41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_arduino/data/script.js: -------------------------------------------------------------------------------- 1 | var maxWifiNetworks = 5; 2 | var peers = []; 3 | 4 | function init() { 5 | $('#config').hide(); 6 | $('#nextstep').hide(); 7 | $('#alert-text').hide(); 8 | 9 | $('#save-button').click(onSaveButtonClicked); 10 | $('#password').keypress(onKeyPressed); 11 | 12 | $('#networks-list-select').attr('disabled', true); 13 | $('#password').attr('disabled', true); 14 | 15 | $.getJSON('/yoyo/credentials', function (json) { 16 | $('#config').show(); 17 | configure(json); 18 | }).fail(function() { 19 | $('#alert-text').show(); 20 | $('#alert-text').addClass('alert-danger'); 21 | $('#alert-text').text('Error'); 22 | }); 23 | } 24 | 25 | function configure(json) { 26 | $('#config').show(); 27 | 28 | console.log(json); 29 | 30 | populateNetworksList(); 31 | populatePeersList(); 32 | } 33 | 34 | function onKeyPressed(event) { 35 | if (event.keyCode == 13) { 36 | onSaveButtonClicked(event); 37 | } 38 | } 39 | 40 | function populateNetworksList(selectedNetwork) { 41 | let networks = $('#networks-list-select'); 42 | 43 | $.getJSON('/yoyo/networks', function (json) { 44 | if(json.length > 0) { 45 | networks.empty(); 46 | //Order the networks by signal strength and limit to top n 47 | json = json.sort((a, b) => parseInt(b.RSSI) - parseInt(a.RSSI)); 48 | var ssidList = json.slice(0, maxWifiNetworks).map(i => { 49 | return i.SSID; 50 | }); 51 | 52 | //The selected network will always remain: 53 | if(selectedNetwork && !ssidList.includes(selectedNetwork)) ssidList.push(selectedNetwork); 54 | 55 | $.each(ssidList, function (key, entry) { 56 | let network = $(''); 57 | 58 | network.attr('value', entry).text(entry); 59 | if(entry == selectedNetwork) network.attr('selected', true); 60 | 61 | networks.append(network); 62 | }); 63 | 64 | $('#networks-list-select').attr('disabled', false); 65 | $('#password').attr('disabled', false); 66 | } 67 | 68 | if($('#networks-list-select option').length == 0) { 69 | networks.append(''); 70 | } 71 | 72 | setTimeout(function() { 73 | populateNetworksList($('#networks-list-select').children("option:selected").val()); 74 | }, 10000); 75 | }); 76 | } 77 | 78 | function populatePeersList() { 79 | $.getJSON('/yoyo/peers', function (json) { 80 | if(json.length > 0) { 81 | var newPeers = json.map(i => { return i.IP;}); 82 | newPeers.forEach(ip => { if(!peers.includes(ip)) addPeer(ip); }); 83 | peers.forEach(ip => { if(!newPeers.includes(ip)) removePeer(ip); }); 84 | 85 | peers = newPeers; 86 | } 87 | }); 88 | 89 | setTimeout(function() { 90 | populatePeersList(); 91 | }, 15000); 92 | } 93 | 94 | function addPeer(ip) { 95 | console.log("addPeer > " + ip); 96 | 97 | let peersListDiv = $('#peers-list'); 98 | 99 | let span = $(''); 100 | span.attr('id', ip); 101 | 102 | let image = $(''); 103 | image.attr('class', 'peer-image'); 104 | image.attr('src', "http://" + ip + "/icon.svg"); 105 | 106 | peersListDiv.append(span); 107 | span.append(image); 108 | } 109 | 110 | function removePeer(ip) { 111 | console.log("removePeer > " + ip); 112 | 113 | $('span[id="' + ip + '"]').remove(); 114 | } 115 | 116 | function onSaveButtonClicked(event) { 117 | event.preventDefault(); 118 | 119 | var data = { 120 | ssid: $('#networks-list-select').children("option:selected").val(), 121 | password: $('#password').val() 122 | }; 123 | 124 | var request = { 125 | type: "POST", 126 | url: "/yoyo/credentials", 127 | data: JSON.stringify(data), 128 | dataType: 'json', 129 | contentType: 'application/json; charset=utf-8', 130 | cache: false, 131 | timeout: 15000, 132 | async: false, 133 | success: function(response, textStatus, jqXHR) { 134 | console.log(response); 135 | $('#config').hide(); 136 | $('#alert-text').show(); 137 | $('#alert-text').removeClass('alert-danger'); 138 | $('#alert-text').addClass('alert-success'); 139 | $('#alert-text').text('Saved'); 140 | $('#nextstep').show(); 141 | }, 142 | error: function (jqXHR, textStatus, errorThrown) { 143 | console.log(jqXHR); 144 | console.log(textStatus); 145 | console.log(errorThrown); 146 | $('#alert-text').show(); 147 | $('#alert-text').addClass('alert-danger'); 148 | $('#alert-text').text('Couldn\'t Save'); 149 | } 150 | } 151 | 152 | //json validation fails on Safari - but if defaults to text then fails on Windows/Android 153 | if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && navigator.userAgent.indexOf('Chromium') == -1) { 154 | request.dataType = 'text'; 155 | } 156 | 157 | $.ajax(request); 158 | } -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_arduino/data/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | background-color: #f5f5f5; 4 | } 5 | 6 | canvas { 7 | display: block; 8 | } 9 | 10 | .yo-yo-form { 11 | width: 100%; 12 | max-width: 330px; 13 | padding: 15px; 14 | margin: 0 auto; 15 | } 16 | .yo-yo-form .checkbox { 17 | font-weight: 400; 18 | } 19 | .yo-yo-form .form-control { 20 | position: relative; 21 | box-sizing: border-box; 22 | padding: 10px; 23 | font-size: 16px; 24 | } 25 | .yo-yo-form .form-control:focus { 26 | z-index: 2; 27 | } 28 | .yo-yo-form input[type="email"], .yo-yo-form select { 29 | margin-bottom: -1px; 30 | border-bottom-right-radius: 0; 31 | border-bottom-left-radius: 0; 32 | } 33 | .yo-yo-form input[type="password"] { 34 | margin-bottom: 10px; 35 | border-top-left-radius: 0; 36 | border-top-right-radius: 0; 37 | } 38 | 39 | .dropdown-menu.pull-left { 40 | left: 0; 41 | } 42 | 43 | .peers { 44 | height: 130px; 45 | } 46 | 47 | .peer-image { 48 | height: 100%; 49 | padding-left: 10px; 50 | padding-right: 10px; 51 | } -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_processing/Device.pde: -------------------------------------------------------------------------------- 1 | /* 2 | Three WiFi Meters - Traffic Monitor 3 | - 4 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 5 | MIT License - Copyright (c) March 2021 6 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-traffic-monitor 7 | */ 8 | 9 | class Device { 10 | String macAddress; 11 | String manufacturer; 12 | int position; 13 | long lastActiveMs; 14 | }; 15 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_processing/Observation.pde: -------------------------------------------------------------------------------- 1 | /* 2 | Three WiFi Meters - Traffic Monitor 3 | - 4 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 5 | MIT License - Copyright (c) March 2021 6 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-traffic-monitor 7 | */ 8 | 9 | class Observation { 10 | int payloadLengthInBytes; 11 | long timeMs; 12 | 13 | Observation(int l, long t) { 14 | payloadLengthInBytes = l; 15 | timeMs = t; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_processing/Observations.pde: -------------------------------------------------------------------------------- 1 | /* 2 | Three WiFi Meters - Traffic Monitor 3 | - 4 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 5 | MIT License - Copyright (c) March 2021 6 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-traffic-monitor 7 | */ 8 | 9 | class Observations { 10 | private int durationMs = 0; 11 | private ArrayList traffic = new ArrayList(); 12 | 13 | Observations(int durationMs) { 14 | this.durationMs = durationMs; 15 | } 16 | 17 | void add(int lengthInBytes, long timeMs){ 18 | if(lengthInBytes > 0) { 19 | traffic.add(new Observation(lengthInBytes, timeMs)); 20 | } 21 | } 22 | 23 | void update() { 24 | try { 25 | for (int i = 0; i < traffic.size();) { 26 | Observation o = traffic.get(i); 27 | if((millis() - durationMs) > o.timeMs) { 28 | traffic.remove(i); 29 | } 30 | else i++; 31 | } 32 | } 33 | catch(Exception e) {} 34 | } 35 | 36 | int count(long startMs, long endMs) { 37 | int total = 0; 38 | 39 | try { 40 | for (int i = 0; i < traffic.size(); i++) { 41 | Observation o = traffic.get(i); 42 | if(o.timeMs < endMs) { 43 | if(o.timeMs > startMs) { 44 | total += o.payloadLengthInBytes; 45 | } 46 | } 47 | } 48 | } 49 | catch(Exception e) {} 50 | 51 | return(total); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_processing/TrafficMonitor_processing.pde: -------------------------------------------------------------------------------- 1 | /* 2 | Three WiFi Meters - Traffic Monitor 3 | - 4 | David Chatting - github.com/davidchatting/ThreeWiFiMeters 5 | MIT License - Copyright (c) March 2021 6 | Documented here > https://github.com/davidchatting/ThreeWiFiMeters#-traffic-monitor 7 | */ 8 | 9 | import processing.serial.*; 10 | import java.util.Map; 11 | 12 | int cx, cy; 13 | int consoleWidth, consoleHeight; 14 | int portalDiameter; 15 | int devicesRingRadius; 16 | int graphMinDiameter; 17 | int graphMaxDiameter; 18 | int graphAxisDiameter; 19 | 20 | Serial sniffer; 21 | String serialPort = null; 22 | int reconnectIntervalMs = 10000; 23 | long reconnectDueAtMs = 0; 24 | 25 | HashMap ouiTable = new HashMap(); 26 | HashMap devices = new HashMap(); 27 | 28 | boolean positionAllocation[] = new boolean[60]; 29 | 30 | long nowMs = 0; 31 | int currentInterval = 0; 32 | int frameRate = 12; 33 | 34 | int requestObservationsIntervalMs = 500; 35 | long requestObservationsDueAtMs = 0; 36 | 37 | int cycles = 3 + 1; 38 | int samplesPerCycle = 60; 39 | float[] ticks = new float[(cycles - 1) * samplesPerCycle]; 40 | int interval; 41 | 42 | Observations uploadTraffic = new Observations(cycles * 60 * 1000); 43 | Observations downloadTraffic = new Observations(cycles * 60 * 1000); 44 | 45 | int flashThresholdMs = (int) (1000 * 0.3f); 46 | 47 | PFont font; 48 | int textHeightPx = 10; 49 | int consoleLineNumber = 1; 50 | 51 | String currentStatus = "YY_DISCONNECTED"; 52 | 53 | void setup() { 54 | frameRate(frameRate); 55 | noCursor(); 56 | 57 | size(320, 480, JAVA2D); 58 | background(0); 59 | smooth(2); 60 | 61 | cx = int(width/2); 62 | cy = int(height-(width/2)) + 10; 63 | portalDiameter = min(width, height)-28; 64 | 65 | interval = ((portalDiameter/2)/(cycles-1))/2; 66 | 67 | devicesRingRadius = (portalDiameter - 8)/2; 68 | 69 | graphMinDiameter = 32; 70 | graphMaxDiameter = portalDiameter - 32; 71 | 72 | graphAxisDiameter = 64; 73 | 74 | consoleWidth = width; 75 | consoleHeight = cy - (portalDiameter/2); 76 | 77 | font = createFont("Courier", textHeightPx); 78 | textFont(font); 79 | 80 | loadOuiTable(); 81 | connectSniffer(); 82 | 83 | nowMs = (millis()/1000) * 1000; 84 | } 85 | 86 | void draw() { 87 | background(0); 88 | 89 | updateTicks(); 90 | 91 | try { 92 | drawConsole(0, 0, consoleWidth, consoleHeight); 93 | 94 | noFill(); 95 | stroke(20, 255); 96 | circle(cx, cy, 8 ); 97 | circle(cx, cy, portalDiameter); 98 | 99 | drawDevices(cx, cy); 100 | drawSpiral(cx, cy, graphMaxDiameter); 101 | } 102 | catch(Exception e) { 103 | println(e); 104 | } 105 | 106 | if (millis() > reconnectDueAtMs) connectSniffer(); 107 | requestObservations(); 108 | } 109 | 110 | void serialEvent(Serial port) { 111 | readObservations(); 112 | } 113 | 114 | void connectSniffer() { 115 | try { 116 | serialPort = getArduinoPort(); 117 | sniffer = new Serial(this, serialPort, 115200); 118 | sniffer.bufferUntil('\n'); 119 | } 120 | catch(Exception e) { 121 | } 122 | 123 | delay(1000); 124 | } 125 | 126 | void requestObservations() { 127 | if (sniffer != null && millis() > requestObservationsDueAtMs) { 128 | sniffer.write('x'); 129 | requestObservationsDueAtMs = millis() + requestObservationsIntervalMs; 130 | 131 | uploadTraffic.update(); 132 | downloadTraffic.update(); 133 | } 134 | } 135 | 136 | void readObservations() { 137 | if (sniffer != null) { 138 | while (sniffer.available() > 0) { 139 | String reading = sniffer.readStringUntil('\n'); 140 | 141 | if (reading != null && reading.startsWith("[aprx]")) { 142 | 143 | String s[] = reading.split("\t"); //[aprx] YY_IDLE_STATUS 54:60:09:E4:B0:BC 2324 144 | if (s.length == 2 || s.length == 5) { 145 | currentStatus = s[1].trim(); 146 | } 147 | 148 | if (s.length == 5) { 149 | addObservation(s[2].trim(), int(s[3].trim()), int(s[4].trim())); 150 | } 151 | 152 | reconnectDueAtMs = millis() + reconnectIntervalMs; 153 | } 154 | } 155 | } 156 | } 157 | 158 | void drawConsole(int x, int y, int w, int h) { 159 | stroke(200, 255); 160 | fill(200, 255); 161 | textAlign(LEFT); 162 | 163 | resetConsoleLine(); 164 | drawConsoleLine(x, y, w, h, ((millis() < reconnectDueAtMs)?"CONNECTED":"NOT CONNECTED") + "\t(" + ((serialPort!=null)?(serialPort):"NONE") + ")"); 165 | drawConsoleLine(x, y, w, h, "-"); 166 | 167 | for (Map.Entry me : devices.entrySet()) { 168 | Device thisDevice = (Device) me.getValue(); 169 | 170 | if (isDevice(thisDevice.macAddress) && thisDevice.lastActiveMs > (millis() - 60000)) { 171 | fill(200); 172 | if (thisDevice.lastActiveMs > (millis() - flashThresholdMs)) { 173 | fill(255); 174 | } 175 | drawConsoleLine(x, y, w, h, thisDevice.macAddress + ((thisDevice.manufacturer == null) ? "" : " " + thisDevice.manufacturer)); 176 | } 177 | } 178 | } 179 | 180 | void drawConsoleLine(int x, int y, int w, int h, String s) { 181 | int maxConsoleLineNumber = (int)(h/(textHeightPx*1.2f)); 182 | 183 | if(consoleLineNumber < maxConsoleLineNumber) { 184 | text(s, x + 10, y + ((textHeightPx + 2) * consoleLineNumber)); 185 | consoleLineNumber++; 186 | } 187 | } 188 | 189 | void resetConsoleLine() { 190 | consoleLineNumber = 1; 191 | } 192 | 193 | void updateTicks() { 194 | int tickIntervalMs = 60000 / samplesPerCycle; 195 | int t = (int)(samplesPerCycle * (second()/60.0f)); 196 | 197 | if (t != currentInterval) { 198 | for (int n = ticks.length-1; n > 0; --n) ticks[n] = ticks[n-1]; 199 | 200 | int dtSec = -1; 201 | long startMs = nowMs + (dtSec * tickIntervalMs); 202 | long endMs = startMs + tickIntervalMs; 203 | 204 | int up = uploadTraffic.count(startMs, endMs); 205 | int down = downloadTraffic.count(startMs, endMs); 206 | ticks[0] = normalise(up + down) * interval/2.0f; 207 | 208 | currentInterval = t; 209 | nowMs += tickIntervalMs; 210 | } 211 | } 212 | 213 | void drawSpiral(int cx, int cy, int diameter) { 214 | noFill(); 215 | 216 | float x, y; 217 | float da = (TWO_PI/samplesPerCycle); 218 | 219 | float r = 0; 220 | float step=(diameter/2.0f)/(samplesPerCycle * (cycles + 1)); 221 | 222 | for (int n = 0; n < (samplesPerCycle * (cycles - 1)); n++ ) { 223 | int i = (samplesPerCycle * cycles) - n; 224 | 225 | r = i * step; 226 | 227 | strokeWeight(i/samplesPerCycle); 228 | 229 | float a = PI - (da*i); 230 | x = r * sin(a); 231 | y = r * cos(a); 232 | 233 | color c = color(0); 234 | float v = ticks[n]; 235 | 236 | if (n == 0) c = color(255, 0, 0); 237 | else { 238 | c = color((v == 0.0f)? 20 : 255); 239 | } 240 | drawTick(cx + x, cy + y, a, v, c); 241 | } 242 | 243 | if(!currentStatus.equals("YY_CONNECTED")) { 244 | fill(255); 245 | textAlign(CENTER, CENTER); 246 | text("CONFIGURE WIFI AT:\nHome Network Study\n-\nblinkblink", cx, cy); 247 | } 248 | } 249 | 250 | void drawTick(float cx, float cy, float a, float l, color c) { 251 | float cxb, cyb; 252 | 253 | if (l == 0) l = 1.0f;//0.1f; 254 | 255 | cxb = cx + (l * sin(a)); 256 | cyb = cy + (l * cos(a)); 257 | 258 | stroke(c); 259 | line(cx, cy, cxb, cyb); 260 | } 261 | 262 | void drawDevices(int cx, int cy) { 263 | strokeWeight(1); 264 | 265 | for (Map.Entry me : devices.entrySet()) { 266 | Device thisDevice = (Device) me.getValue(); 267 | 268 | if (isDevice(thisDevice.macAddress) && thisDevice.lastActiveMs > (millis() - 60000)) { 269 | float a = map(thisDevice.position, 0, 60, 0, TWO_PI); 270 | int x = cx + (int)(devicesRingRadius * sin(a)); 271 | int y = cy + (int)(devicesRingRadius * cos(a)); 272 | 273 | stroke(255); 274 | if (thisDevice.lastActiveMs > (millis() - flashThresholdMs)) { 275 | fill(255); 276 | } else noFill(); 277 | circle(x, y, 8); 278 | } 279 | } 280 | } 281 | 282 | int allocatePosition(String macAddress) { 283 | int position = -1; 284 | 285 | if(isDevice(macAddress)){ 286 | String bytes[] = macAddress.split(":"); 287 | position = (int)map(unhex(bytes[5]), 0, 255, 0, 59); 288 | 289 | while (positionAllocation[position]) { 290 | position = (position + 1) % 60; 291 | } 292 | positionAllocation[position] = true; 293 | } 294 | 295 | return(position); 296 | } 297 | 298 | float normalise(int v) { 299 | float result = 0.0f; 300 | 301 | if (v > 0) { 302 | //approximation of the sigmoid function: 303 | result = map(v, 0, 8192, -6.0f, 6.0f); 304 | result = map(result / (1.0f + abs(result)), -1.0f, 1.0f, 0.0f, 1.0f); 305 | } 306 | 307 | return(result); 308 | } 309 | 310 | void addObservation(String macAddress, int uploadBytes, int downloadBytes) { 311 | long now = millis(); 312 | Device thisDevice = devices.get(macAddress); 313 | if (thisDevice == null) { 314 | thisDevice = new Device(); 315 | thisDevice.macAddress = macAddress; 316 | thisDevice.manufacturer = ouiTable.get(getOUI(macAddress)); 317 | 318 | //Ignore devices without a known manufacturer and other ESP devices 319 | thisDevice.position = allocatePosition(macAddress); 320 | devices.put(macAddress, thisDevice); 321 | } 322 | thisDevice.lastActiveMs = now; 323 | 324 | uploadTraffic.add(uploadBytes, now); 325 | downloadTraffic.add(downloadBytes, now); 326 | } 327 | 328 | boolean isDevice(String macAddress) { 329 | boolean result = true; 330 | 331 | result = result && !macAddress.endsWith("00:00:00"); //group address 332 | result = result && !macAddress.equals("FF:FF:FF:FF:FF:FF"); //broadcast address 333 | result = result && !macAddress.startsWith("01:00:5E"); //IPv4 multicast address 334 | result = result && !macAddress.startsWith("33:33"); //IPv6 multicast address 335 | result = result && !macAddress.startsWith("01:80:C2"); //Bridge address 336 | 337 | return(result); 338 | } 339 | 340 | int getOUI(String macAddress) { 341 | int oui = 0; 342 | 343 | String bytes[] = macAddress.split(":"); 344 | oui = (unhex(bytes[0]) << 16) + (unhex(bytes[1]) << 8) + unhex(bytes[2]); 345 | 346 | return(oui); 347 | } 348 | 349 | void loadOuiTable() { 350 | //http://linuxnet.ca/ieee/oui/nmap-mac-prefixes 351 | 352 | String[] prefixes = loadStrings("nmap-mac-prefixes"); 353 | for (int n=0; n < prefixes.length; ++n) { 354 | if(!prefixes[n].startsWith("#")) { 355 | String[] p = prefixes[n].split("\t"); 356 | ouiTable.put(unhex(p[0]), p[1]); 357 | } 358 | } 359 | } 360 | 361 | String getArduinoPort() { 362 | String port = null; 363 | 364 | String serialList [] = Serial.list(); 365 | for (int n=0; n < serialList.length && port==null; ++n) { 366 | if (looksLikeArduino(serialList[n])) { 367 | port = serialList[n]; 368 | } 369 | } 370 | 371 | return(port); 372 | } 373 | 374 | boolean looksLikeArduino(String s) { 375 | return(s.equals("/dev/serial0") || s.startsWith("/dev/tty.usb") || s.equals("/dev/cu.SLAB_USBtoUART") || s.startsWith("/dev/ttyUSB")); 376 | } 377 | -------------------------------------------------------------------------------- /TrafficMonitor/TrafficMonitor_processing/data/make-mac-prefixes.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use POSIX; 3 | 4 | # Minor adaptation of script distribited at nmap.org 5 | 6 | # A simple perl script that takes a MAC address database as distribted 7 | # by the IEEE at http://standards.ieee.org/regauth/oui/oui.txt and 8 | # creates an nmap-mac-prefixes file, which is just a bunch of lines 9 | # like this (but without the initial "# ": 10 | # 11 | # 000072 Miniware Technology 12 | # 00012E PC Partner Ltd. 13 | # 080023 Panasonic Communications Co., Ltd. 14 | # 15 | 16 | sub usage() { 17 | print "usage: make-mac-prefixes.pl [infile] [outfile]\n" . 18 | "where infile is usually oui.txt as distributed from\n" . 19 | "http://standards.ieee.org/regauth/oui/oui.txt and outfile is usually\n" . 20 | "nmap-mac-prefixes. The output file will be overwritten if it already exists.\n"; 21 | exit 1; 22 | } 23 | 24 | # Un-capitalize an all-caps company name; 25 | sub decap($) { 26 | my $oldcomp = shift(); 27 | my $newcomp = ""; 28 | my @words = split /\s/, $oldcomp; 29 | foreach $word (@words) { 30 | if (length($word) > 3 && (length($word) > 5 or !($word =~ /[.,\!\$]/))) { 31 | $word = "\L$word\E"; 32 | $word = "\u$word"; 33 | } 34 | if ($newcomp) { $newcomp .= " $word"; } 35 | else {$newcomp = $word; } 36 | } 37 | 38 | return $newcomp; 39 | } 40 | 41 | # Rules to shorten the names a bit, such as eliminating Inc. 42 | sub shorten($) { 43 | my $comp = shift(); 44 | $comp =~ s/,.{1,6}$//; 45 | $comp =~ s/ (Corporation|Inc|Ltd|Corp|S\.A\.|Co\.|llc|pty|l\.l\.c\.|s\.p\.a\.|b\.v\.)(\.|\b)//gi; 46 | # Fix stupid entries like "DU PONT PIXEL SYSTEMS ." 47 | $comp =~ s/\s+.$//; 48 | return $comp; 49 | } 50 | 51 | my $infile = shift() || usage(); 52 | my $outfile = shift() || usage(); 53 | 54 | if (! -f $infile) { print "ERROR: Could not find input file $infile"; usage(); } 55 | 56 | open INFILE, "<$infile" or die "Could not open input file $infile"; 57 | open OUTFILE, ">$outfile" or die "Could not open output file $outfile"; 58 | 59 | print OUTFILE "# \$Id" . ": \$ generated with make-mac-prefixes.pl\n"; 60 | print OUTFILE "# Original data comes from http://standards.ieee.org/regauth/oui/oui.txt\n"; 61 | print OUTFILE "# These values are known as Organizationally Unique Identifiers (OUIs)\n"; 62 | print OUTFILE "# See http://standards.ieee.org/faqs/OUI.html\n"; 63 | 64 | while($ln = ) { 65 | if ($ln =~ /\s*([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})\s+\(hex\)\s+(\S.*)$/) { 66 | my $prefix = "$1$2$3"; 67 | my $compname= $4; 68 | # This file often over-capitalizes company names 69 | if (!($compname =~ /[a-z]/) || $compname =~ /\b[A-Z]{4,}/) { 70 | $compname = decap($compname); 71 | } 72 | $compname = shorten($compname); 73 | print OUTFILE "$prefix\t$compname\n"; 74 | } 75 | # else { print "failed to match: $ln"; } 76 | } 77 | -------------------------------------------------------------------------------- /images/hero-sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/images/hero-sm.gif -------------------------------------------------------------------------------- /images/hero.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/images/hero.gif -------------------------------------------------------------------------------- /images/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchatting/ThreeWiFiMeters/2214543ea08e8626c542736f0be82f6d92079a61/images/hero.png --------------------------------------------------------------------------------