├── .gitattributes ├── LICENSE.txt ├── README.md ├── cfg ├── bundles.json~ ├── bundles.list.json ├── bundles │ └── CBQ2.zephyrcab ├── decoders.js └── settings.json ├── custom.css ├── favicon.ico ├── img ├── GitHub-Mark-120px-plus.png ├── GitHub-Mark-Light-120px-plus.png ├── GitHub_Logo.png └── Railway_track_1920x1080.jpg ├── index.html ├── scripts ├── air.js ├── brakes.js ├── bundles.js ├── buzz │ ├── buzz.js │ └── buzz.min.js ├── github.js ├── jmri-core.js ├── pretty-logs.js ├── setup.js ├── sim.js ├── stats.js ├── train.js ├── ui.js └── websockets.js ├── soundfx ├── click.mp3 └── switch.mp3 ├── standards └── decoders.standards.js └── thirdparty ├── canv-gauge-master ├── README ├── build.bat ├── build.sh ├── compiler.jar ├── example-html-gauge.html ├── example-resize.html ├── example.html ├── fonts │ ├── digital-7-mono.eot │ └── digital-7-mono.ttf ├── gauge.js ├── gauge.min.js └── gauge.min.js.map ├── fonts ├── zephyr.eot ├── zephyr.otf ├── zephyr.svg ├── zephyr.ttf └── zephyr.woff ├── materialize ├── LICENSE ├── README.md ├── css │ ├── materialize-fromSASS.css │ ├── materialize.css │ └── materialize.min.css ├── font │ ├── material-design-icons │ │ ├── LICENSE.txt │ │ ├── Material-Design-Icons.eot │ │ ├── Material-Design-Icons.svg │ │ ├── Material-Design-Icons.ttf │ │ ├── Material-Design-Icons.woff │ │ └── Material-Design-Icons.woff2 │ └── roboto │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Thin.woff │ │ └── Roboto-Thin.woff2 └── js │ ├── materialize.js │ └── materialize.min.js └── octicons ├── LICENSE.txt ├── README.md ├── octicons-local.ttf ├── octicons.css ├── octicons.eot ├── octicons.less ├── octicons.scss ├── octicons.svg ├── octicons.ttf ├── octicons.woff └── sprockets-octicons.scss /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![ZephyrCab Logo](http://i.imgur.com/n07xxtI.png)](http://k4kfh.github.io/ZephyrCab) 2 | 3 | # Project Status: 4 | > I am no longer actively maintaining this project (as of 2024), due to a lack of public interest and my own movement away from the hobby (for now...it probably won't last). If you are interested in picking up the project, please get in touch with me via GitHub. 5 | > - Hampton 6 | 7 | For information on the underlying physics math I used, please see: [*The Idiot's Guide to Railroad Physics*](http://k4kfh.github.io/idiotsGuideToRailroadPhysics) 8 | 9 | [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) 10 | 11 | ZephyrCab is a web app that simulates prototypically accurate controls for model trains, built on the JMRI model train control software. 12 | 13 | 14 | # Quickstart Guide 15 | 16 | ZephyrCab is ready for you to test drive! It is fairly early in development, so if you run into issues please hop in the Gitter chat and I will help you out. 17 | 18 | [![Join the chat at https://gitter.im/k4kfh/LocoThrottleJS](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/k4kfh/LocoThrottleJS?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 19 | 20 | ## Prerequisites 21 | 22 | - **A DCC layout connected to JMRI.** 23 | 24 | No seriously. That's it. 25 | 26 | ## Installation 27 | 28 | The screenshots below are from a machine running Linux, so they may look a little different, but the procedure will be essentially the same for Windows, Mac, and Linux. 29 | 30 | 1. [**Download the latest ZephyrCab release here.**](https://github.com/k4kfh/ZephyrCab/archive/master.zip) 31 | 2. **Find your JMRI profile directory.** You can do this by opening JMRI and clicking Help > Locations, as shown below. 32 | 33 | ![](http://imgur.com/enSiiful.png) 34 | 35 | 3. **Open your JMRI profile directory.** You can just click "Open profile location" from inside the JMRI Locations dialog. In my case, my profile directory was ``/home/hampton/.jmri/My_JMRI_Railroad``, but yours may be a little different. 36 | 37 | ![](http://imgur.com/HwbhQ8nl.png) 38 | 39 | 4. **Create a folder called ``web`` inside the profile location.** Your system may already have this folder, but if it doesn't, just make a new folder called ``web``. 40 | 41 |
42 | 43 | ![](http://imgur.com/TqVgcEbl.png) 44 | 45 | 5. **Extract the ZephyrCab download into the ``web`` folder.** When you downloaded ZephyrCab, you should have gotten a ZIP file, so just extract its contents into ``/wherever/your/JMRI/profile/is/web``. 46 | 47 | 6. **Rename the folder to ``zephyrcab``.** This step is technically optional, but makes things easier, so I recommend it. 48 | 49 | 7. **If you haven't used it before, start your JMRI web server.** You can do this in Edit > Preferences > Web Server. Check the box for "Start automatically with application". 50 | 51 | ![](http://i.imgur.com/5R3EMtE.png) 52 | 53 | 8. **Open your ZephyrCab in a web browser.** Google Chrome is officially supported, though Firefox will probably work. No promises otherwise. 54 | - If you're opening it from your JMRI machine, you can just use [``http://localhost:12080/web/zephyrcab``](http://localhost:12080/web/zephyrcab) 55 | - Otherwise, the URL will be ``http://your-jmri-ip-address:12080/web/zephyrcab`` if you've followed this guide correctly. 56 | - If you don't know your JMRI PC's IP address, [click here to learn how to find it.](http://www.howtogeek.com/236838/how-to-find-any-devices-ip-address-mac-address-and-other-network-connection-details/) It will probably be in the form ``192.168.1.something`` or ``172.16.something.something``, but could be different. 57 | 58 | 9. **Create bundles for your locomotives.** Bundles are the small data files that tell ZephyrCab all the physics information about your locomotive. They also bind it to an actual model on your JMRI roster. You'll need to create a new bundle for your first locomotive, which will probably require a data sheet for information like weight, tractive effort, and horsepower. The "Setup" page within ZephyrCab has an easy tool for creating bundles. 59 | 60 | 10. **Install your bundles.** Once you've created and downloaded the bundle files, you'll need to place them in the ``/cfg/bundles`` folder within ZephyrCab. You _also_ need to add the file names to the ``/cfg/bundles.list.json`` file, otherwise ZephyrCab won't know to load them. So for example, if I created a bundles file called ``BN1379.zephyrcab``, I would first place it in the ``/cfg/bundles`` folder. Then I would edit the list at the bottom of ``bundles.list.json`` to look like this. 61 | 62 | ```javascript 63 | bundles.files = [ 64 | "BN1379.zephyrcab", 65 | ] 66 | ``` 67 | 68 | Once you get your bundles set up, ZephyrCab should be ready to go. Simply go to the "Train Settings" tab and add your locomotive/cars. Note that some locomotives have more advanced sound support than others (for example, ZephyrCab knows how to use the prime mover manual notching feature on certain ESU decoders). All decoders will work, but you may only get speed/direction/lighting control on decoders that I haven't had a chance to properly code for yet. If you run into problems, post an issue on [the project's GitHub page](http://github.com/k4kfh/ZephyrCab), or join the support chat [on Gitter](https://gitter.im/k4kfh/ZephyrCab). 69 | 70 | ## Additional Help 71 | 72 | Please see [the ZephyrCab documentation](http://k4kfh.github.io/ZephyrCab/docs/site) for more detailed information on configuration tasks such as setting up automatic connection, adding locomotives, tweaking brake system defaults, and other options. You can also ask questions by creating an issue on GitHub, or [joining the Gitter chat.](https://gitter.im/k4kfh/ZephyrCab) 73 | 74 | ## Built With 75 | 76 | * [MaterializeCSS](http://materializecss.com) 77 | * [jQuery](http://jquery.com) 78 | * [JMRI](http://jmri.org) 79 | * [mkDocs](http://www.mkdocs.org/) 80 | * [mkDocs Material Theme by squidFunk](http://squidfunk.github.io/mkdocs-material/) 81 | 82 | ## Contributing 83 | 84 | Any and all contributions are welcome. I am working on better documentation for contributors, but in the meantime feel free to make an issue if you have questions about contributing. 85 | 86 | ## Acknowledgments 87 | 88 | Hats off to: 89 | - [Mr. Bruce Kingsley](http://brucekmodeltrains.com), for _incredible_ help and insight on the physics 90 | - Mr. Al Krug, for excellent reading material, particularly on railway brakes 91 | - [JMRI](http://jmri.org), for the excellent JSON/WebSockets API that makes this project possible 92 | - [MaterializeCSS](http://materializecss.com), for a wonderful free Material Design CSS framework 93 | -------------------------------------------------------------------------------- /cfg/bundles.list.json: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | bundles = new Object(); 19 | /* 20 | BUNDLES LIST: 21 | You can store all your bundles files in /cfg/bundles. All you need to do here is add the filenames so that ZephyrCab knows where to find them. 22 | 23 | EXAMPLE: If you have the files CBQ2.zephyrcab and SW1000.zephyrcab, you should do this: 24 | 25 | bundles.files = [ 26 | "CBQ2.zephyrcab", 27 | "SW1000.zephyrcab", 28 | ] 29 | */ 30 | bundles.files = [ 31 | "CBQ2.zephyrcab", 32 | ] -------------------------------------------------------------------------------- /cfg/bundles/CBQ2.zephyrcab: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | tmp = {"CBQ2" : { 19 | "type": "locomotive", 20 | "prototype": { 21 | "builder": "EMD", 22 | "name": "F7-A", 23 | "weight": 250000, 24 | "maxHP": 1500, 25 | "maxAmps": 900, 26 | "scaleSpeedCoefficient": 0.7268, 27 | "notchRPM": [ 28 | 300, 29 | 362, 30 | 425, 31 | 487, 32 | 550, 33 | 613, 34 | 675, 35 | 738, 36 | 800 37 | ], 38 | "notchMaxSpeeds": [ 39 | null, 40 | 7.5, 41 | 15, 42 | 22.5, 43 | 30, 44 | 37.5, 45 | 45, 46 | 52.5, 47 | 60 48 | ], 49 | "engineRunning": 0, 50 | "startingTE": 56500, 51 | "drivetrainEfficiency": 0.72, 52 | "wheelSlip": { 53 | "adhesion": 0.3, 54 | "adhesionDuringSlip": 0.25 55 | }, 56 | "air": { 57 | "reservoir": { 58 | "main": { 59 | "capacity": 46.8, 60 | "leakRate": 0 61 | } 62 | }, 63 | "compressor": { 64 | "limits": { 65 | "lower": 130, 66 | "upper": 140 67 | }, 68 | "flowrateCoeff": 0.28 69 | } 70 | }, 71 | "coeff": { 72 | "rollingResistance": 0.0015 73 | }, 74 | "brake": { 75 | "latency": 100 76 | } 77 | } 78 | }}; -------------------------------------------------------------------------------- /cfg/decoders.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | /* 19 | DECODERS 20 | 21 | This file is user-editable. It contains all the decoder objects. For developers: these objects are constructors that are called any time a locomotive is added to the train with a corresponding decoder. The decoder functions all go in train.all[number].dcc . These decoder objects provide the necessary abstraction layer for the rest of ZephyrCab to have easy access to all the sound features, such as compressors, air dumps, bells, and horns. Decoder information is not ingested from manual user input anymore; it is automatically fetched from the JMRI roster. The corresponding decoder object is looked up in this file using JMRI's naming convention. This means it is imperative that the names of your decoder objects be precisely correct. 22 | 23 | If ZephyrCab can't find a decoder object for a locomotive in your roster, it will fall back to a "generic" entry which has no sound support and limited lighting/direction/speed support. DO NOT DELETE THE GENERIC ENTRY! **This feature is currently a work in progress as of June 2016. Learn more on the project's GitHub page.** 24 | */ 25 | 26 | //JSlint Crap 27 | /*global 28 | foo, WebSocket, $, Materialize, console, cfg, train, jmri, ui, air, sim 29 | */ 30 | /*jslint browser:true, white:true, plusplus:true*/ 31 | 32 | var decoders = { 33 | //product "LokSound Select" 34 | "ESU LokSound Select": { 35 | //sound project "emd567" 36 | "LokSound Select EMD 567": function(address, trainPosition) { 37 | //ESU LokSound Select V4 38 | //decoder object for ESU official EMD 567 Sound project 39 | //By Hampton Morgan - k4kfh@github - Originally written in May 2015 40 | //evilgeniustech.com 41 | log.decoder("Using 'LokSound Select EMD 567' for " + trainPosition) 42 | train.all[trainPosition].throttle = new jmri.throttle(address, jmri.throttleName.generate()); //we use the train position as the throttle name for future lookup purposes 43 | 44 | //FUNCTIONS 45 | this.f = {}; 46 | //light 47 | this.f.headlight = {}; 48 | this.f.headlight.set = function(state) { 49 | train.all[trainPosition].throttle.f.set({ 50 | "F0": state 51 | }); 52 | train.all[trainPosition].dcc.f.headlight.state = state; 53 | log.decoder(" Setting headlight to " + state + " on Train#" + trainPosition) 54 | }; 55 | //bell 56 | this.f.bell = {}; 57 | this.f.bell.set = function(state) { 58 | train.all[trainPosition].throttle.f.set({ 59 | "F1": state 60 | }); 61 | train.all[trainPosition].dcc.f.bell.state = state; 62 | log.decoder(" Setting bell to " + state + " on Train#" + trainPosition) 63 | }; 64 | this.f.bell.state = false; 65 | 66 | //horn 67 | this.f.horn = {}; 68 | this.f.horn.set = function(state) { 69 | train.all[trainPosition].throttle.f.set({ 70 | "F2": state 71 | }); 72 | train.all[trainPosition].dcc.f.horn.state = state; 73 | log.decoder("Setting headlight to " + state + " on Train#" + trainPosition) 74 | }; 75 | this.f.horn.state = false; 76 | 77 | //compressor 78 | this.f.compressor = {}; 79 | this.f.compressor.set = function(state) { 80 | if (state != train.all[trainPosition].dcc.f.compressor.state) { 81 | train.all[trainPosition].throttle.f.set({ 82 | "F20": state 83 | }); 84 | train.all[trainPosition].dcc.f.compressor.state = state; 85 | log.decoder("Setting compressor to " + state + " on Train#" + trainPosition) 86 | } 87 | }; 88 | this.f.compressor.state = false; 89 | 90 | //air release 91 | this.f.airDump = {}; 92 | this.f.airDump.set = function(state) { 93 | train.all[trainPosition].throttle.f.set({ 94 | "F19": state 95 | }); 96 | log.decoder("Setting airDump to " + state + " on Train#" + trainPosition) 97 | }; 98 | 99 | //dyn brake fans 100 | this.f.dynBrakes = {}; 101 | this.f.dynBrakes.set = function(state) { 102 | 103 | }; 104 | this.f.dynBrakes.state = false; 105 | 106 | //engine on/off 107 | this.f.engine = {}; 108 | this.f.engine.set = function(state) { 109 | //This IF makes the entire function useless if you're out of fuel, or if the state argument is no different than the current actual state 110 | if (state !== train.all[trainPosition].dcc.f.engine.state) { 111 | train.all[trainPosition].throttle.f.set({ 112 | "F8": state 113 | }); 114 | train.all[trainPosition].dcc.f.engine.state = state; 115 | log.decoder("Setting engine to " + state + " on Train#" + trainPosition) 116 | //This code sets engineRunning to 0 or 1 depending on the state 117 | if (state === true) { 118 | train.all[trainPosition].prototype.engineRunning = 1; 119 | } else if (state === false) { 120 | train.all[trainPosition].prototype.engineRunning = 0; 121 | } 122 | } else { 123 | //This code means that if you're out of fuel, regardless of what state you fed into this function it will turn the engine off. 124 | train.all[trainPosition].throttle.f.set({ 125 | "F8": false 126 | }); 127 | train.all[trainPosition].prototype.engineRunning = 0; 128 | train.all[trainPosition].dcc.f.engine.state = false; 129 | } 130 | }; 131 | this.f.engine.state = false; 132 | 133 | //notch sound stuff. 134 | this.f.notch = { 135 | up: function() { 136 | //Notch up code 137 | //This is inside an IF statement to make sure we don't try to notch OVER 8. If that happens, ESU decoders get confused. 138 | var newNotch = (train.all[trainPosition].dcc.f.notch.state + 1); 139 | if (newNotch <= 8) { 140 | train.all[trainPosition].dcc.f.notch.state++; //THIS HAS TO RUN INSTANTLY OR SIM.JS IS STUPID 141 | log.decoder("Increasing notch on Train#" + trainPosition) 142 | setTimeout(function() { 143 | train.all[trainPosition].throttle.f.set({ 144 | "F9": true 145 | }); 146 | }, 500); 147 | setTimeout(function() { 148 | train.all[trainPosition].throttle.f.set({ 149 | "F9": false 150 | }); 151 | }, 1750); 152 | } 153 | }, 154 | down: function() { 155 | //Notch down code 156 | //This is inside an IF statement to make sure we don't try to notch LESS THAN idle. If that happens, ESU decoders get confused. 157 | var newNotch = (train.all[trainPosition].dcc.f.notch.state - 1); 158 | if (newNotch >= 0) { 159 | train.all[trainPosition].dcc.f.notch.state--; //THIS MUST RUN INSTANTLY OR SIM.JS DOES WEIRD STUFF 160 | log.decoder("Decreasing notch on Train#" + trainPosition) 161 | setTimeout(function() { 162 | train.all[trainPosition].throttle.f.set({ 163 | "F10": true 164 | }); 165 | }, 500); 166 | setTimeout(function() { 167 | train.all[trainPosition].throttle.f.set({ 168 | "F10": false 169 | }); 170 | }, 1750); 171 | } 172 | }, 173 | state: 0 //This should reflect the current notching state of the sound decoder. You should increment this up or down 1 when your up() and down() functions finish, or sim.js's functions will be horribly confused and mess up your sounds. 174 | }; 175 | 176 | 177 | //SPEED SETTING 178 | this.speed = {}; 179 | this.speed.state = 0; 180 | this.speed.set = function(speed) { 181 | train.all[trainPosition].throttle.speed.set(speed); 182 | train.all[trainPosition].dcc.speed.state = speed; 183 | }; 184 | this.speed.setMPH = function(mph, trainPosition) { 185 | var speed = train.all[trainPosition].model.speed(mph, trainPosition); 186 | train.all[trainPosition].dcc.speed.set(speed); 187 | }; 188 | } 189 | }, 190 | 191 | //GENERIC FALLBACK SCRIPT - DO NOT REMOVE!! 192 | "generic": { 193 | "generic": function(address, trainPosition) { 194 | 'use strict'; 195 | log.decoder("Using 'generic' for " + trainPosition) 196 | //GENERIC FALLBACK 197 | train.all[trainPosition].throttle = new jmri.throttle(address, jmri.throttleName.generate()); 198 | 199 | //FUNCTIONS 200 | this.f = {}; 201 | 202 | //light 203 | this.f.headlight = {}; 204 | this.f.headlight.set = function(state) { 205 | train.all[trainPosition].throttle.f.set({ 206 | "F0": state 207 | }); 208 | train.all[trainPosition].dcc.f.headlight.state = state; 209 | }; 210 | this.f.headlight.state = false; 211 | 212 | //bell 213 | this.f.bell = {}; 214 | this.f.bell.set = function(state) { 215 | train.all[trainPosition].throttle.f.set({ 216 | "F1": state 217 | }); 218 | train.all[trainPosition].dcc.f.bell.state = state; 219 | }; 220 | this.f.bell.state = false; 221 | 222 | //horn 223 | this.f.horn = {}; 224 | this.f.horn.set = function(state) { 225 | train.all[trainPosition].throttle.f.set({ 226 | "F2": state 227 | }); 228 | train.all[trainPosition].dcc.f.horn.state = state; 229 | }; 230 | this.f.horn.state = false; 231 | 232 | //compressor 233 | this.f.compressor = {}; 234 | this.f.compressor.set = function(state) { 235 | //there's no compressor assumed on these generic mystery decoders 236 | train.all[trainPosition].dcc.f.compressor.state = state; 237 | }; 238 | this.f.compressor.state = false; 239 | 240 | //air release 241 | this.f.airDump = {}; 242 | this.f.airDump.set = function(state) { 243 | 244 | }; 245 | this.f.airDump.state = false; 246 | 247 | //dyn brake fans 248 | this.f.dynBrakes = {}; 249 | this.f.dynBrakes.set = function(state) { 250 | 251 | }; 252 | this.f.dynBrakes.state = false; 253 | 254 | //engine on/off 255 | this.f.engine = {}; 256 | this.f.engine.set = function(state) { 257 | //This function is almost exactly the same as the one in my ESU LokSound EMD 567 decoder constructor, the difference is this one never actually sends a DCC command (it's basically dummy function that the physics engine thinks is legit) 258 | 259 | train.all[trainPosition].dcc.f.engine.state = state; 260 | //This code sets engineRunning to 0 or 1 depending on the state 261 | if (state === true) { 262 | train.all[trainPosition].prototype.engineRunning = 1; 263 | } else if (state === false) { 264 | train.all[trainPosition].prototype.engineRunning = 0; 265 | } 266 | }; 267 | this.f.engine.state = false; 268 | 269 | //notch sound stuff. 270 | this.f.notch = { 271 | up: function() { 272 | //Notch up code 273 | //This is inside an IF statement to make sure we don't try to notch OVER 8. 274 | var newNotch = (train.all[trainPosition].dcc.f.notch.state + 1); 275 | if (newNotch <= 8) { 276 | train.all[trainPosition].dcc.f.notch.state++; //THIS HAS TO RUN INSTANTLY OR SIM.JS IS STUPID 277 | } 278 | }, 279 | down: function() { 280 | //Notch down code 281 | //This is inside an IF statement to make sure we don't try to notch LESS THAN idle. 282 | var newNotch = (train.all[trainPosition].dcc.f.notch.state - 1); 283 | if (newNotch >= 0) { 284 | train.all[trainPosition].dcc.f.notch.state--; //THIS MUST RUN INSTANTLY OR SIM.JS DOES WEIRD STUFF 285 | } 286 | }, 287 | state: 0 //This should reflect the current notching state of the sound decoder. You should increment this up or down 1 when your up() and down() functions finish, or sim.js's functions will be horribly confused and mess up your sounds. 288 | }; 289 | 290 | 291 | //SPEED SETTING 292 | this.speed = {}; 293 | this.speed.state = 0; 294 | this.speed.set = function(speed) { 295 | train.all[trainPosition].throttle.speed.set(speed); 296 | train.all[trainPosition].dcc.speed.state = speed; 297 | }; 298 | this.speed.setMPH = function(mph) { 299 | var speed = train.all[trainPosition].model.speed(mph, trainPosition); 300 | train.all[trainPosition].dcc.speed.set(speed); 301 | }; 302 | } 303 | } 304 | }; -------------------------------------------------------------------------------- /cfg/settings.json: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | cfg = new Object(); 19 | cfg.brakes = new Object(); 20 | /* 21 | ZEPHYRCAB CONFIGURATION 22 | 23 | This file is used to store settings server-side. This is capable of storing an IP address to connect to automatically, as well as other things. 24 | 25 | BEGIN USER-EDITABLE CONTENT 26 | */ 27 | 28 | /* 29 | CONNECTION SETTINGS 30 | By default, these settings assume that ZephyrCab is running on a local JMRI instance, via the built-in Jetty web server, and ignores the cfg.ip/cfg.port settings. This is the recommended method. 31 | 32 | If you'd prefer to use an external web server, set cfg.webServer to "external" and cfg.ip/cfg.port to your JMRI PC's IP and port. This is not recommended except for experienced users/developers. 33 | */ 34 | cfg.webServer="external"; 35 | cfg.ip = "jmri"; //ignored when cfg.webServer is set to "jmri" 36 | cfg.port = 12080; //ignored when cfg.webServer is set to "jmri" 37 | 38 | cfg.disablePushNotifications = false; //disable push notifications from a central source (on GitHub) 39 | cfg.disableAnonymousUsageData = false; //by default, when this is set to false, ZephyrCab will send some anonymous data (your browser version, how many locomotives you have, etc) back to the developers to gain insight on who is using the program and how to improve it. 40 | cfg.usageDataUsername = "none"; //if you are a developer, you can set this to your GitHub username so that we know which statistics come from your installations 41 | 42 | cfg.debugToasts = false; //enable debugging notifications (developers only) 43 | 44 | cfg.logallmessages = false; //This will log EVERY WebSockets message that is sent or recieved to the console as a string. This is meant for copying/pasting into GitHub issues and such. It prefaces messages we send with "SENT : " and messages from JMRI with "RECIEVED : " 45 | 46 | cfg.brakes.defaultFeedValveSetting = 90; //integer, in psi, for the feed valve to default to. The feed valve can be adjusted once you enter the cab, this just provides an easy way to set a preferred default. 47 | cfg.brakes.notifications = true; //enable this to give you detailed notifications about the brakes on each car (ie "Car #2 brakes finished charging") 48 | 49 | /*END USER-EDITABLE CONTENT*/ 50 | -------------------------------------------------------------------------------- /custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | @font-face { 19 | font-family: 'Zephyr'; 20 | src: url('thirdparty/fonts/zephyr.eot'); 21 | src: local('☺'), url('thirdparty/fonts/zephyr.woff') format('woff'), url('thirdparty/fonts/zephyr.ttf') format('truetype'), url('thirdparty/fonts/zephyr.svg') format('svg'); 22 | 23 | } 24 | 25 | .zephyr { 26 | font-family: 'Zephyr'; 27 | } 28 | 29 | /* 30 | This keeps the scrollbar on the longer pages from screwing with the alignment of things 31 | */ 32 | html { 33 | overflow-y: scroll; 34 | } 35 | 36 | #cab-fullscreen-container.fullscreen { 37 | z-index: 9999; 38 | width:100vw; 39 | height:100vh; 40 | position: fixed; 41 | top: 0; 42 | left: 0; 43 | background-color:white; 44 | padding-top:2vh; 45 | padding-bottom:2vh; 46 | padding-left:5vh; 47 | padding-right:5vh; 48 | } 49 | 50 | .square-btn { 51 | padding-top:5px; 52 | padding-right:5px; 53 | padding-left:5px; 54 | border-radius:6px; 55 | } 56 | 57 | body { 58 | background-color:white; 59 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/favicon.ico -------------------------------------------------------------------------------- /img/GitHub-Mark-120px-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/img/GitHub-Mark-120px-plus.png -------------------------------------------------------------------------------- /img/GitHub-Mark-Light-120px-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/img/GitHub-Mark-Light-120px-plus.png -------------------------------------------------------------------------------- /img/GitHub_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/img/GitHub_Logo.png -------------------------------------------------------------------------------- /img/Railway_track_1920x1080.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/img/Railway_track_1920x1080.jpg -------------------------------------------------------------------------------- /scripts/air.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | /* 19 | AIR UTILITIES 20 | 21 | These are specifically designed to make working with air systems easier. They provide an easy abstraction layer for all the math behind pneumatic simulations. 22 | */ 23 | 24 | var air = { 25 | reservoir : { 26 | main : { 27 | /* 28 | updatePSI(); 29 | 30 | Takes one argument: 31 | locomotive : the position in the train of the loco you want to update (for example, if it was first it would be 0) 32 | 33 | When called, it will look at the various air measurements in .prototype.realtime and calculate the pressure of the main reservoir. It updates both psi abs and psig measurements with the calculation, no need to update them yourself. You will, however, need to update the gauge IF you're in the current loco. 34 | 35 | Returns: 36 | nothing. nothing at all. nice and simple. 37 | */ 38 | updatePSI : function (locomotive) { 39 | var capacity = train.all[locomotive].prototype.air.reservoir.main.capacity; 40 | var currentAtmAirVolume = train.all[locomotive].prototype.air.reservoir.main.currentAtmAirVolume; 41 | 42 | //atmosphere air is at 14.696psi, so: 43 | var psiAbs = (currentAtmAirVolume / capacity) + 13.696; //this means that if currentAtmAirVolume and capacity are equal, then the psi will be 14.696psi (1atm) 44 | var psig = (currentAtmAirVolume / capacity) - 1; //this is psig, which is 0 if capacity and currentAtmAirVolume are equal. It is the difference between tank pressure and ambient (atmosphere) pressure, and is the value displayed on the dash gauge. 45 | 46 | //this is a safeguard to make sure we don't accidentally turn the reservoir into a vacuum 47 | if (psig < 0) { 48 | psiAbs = 14.696; 49 | psig = 0; 50 | } 51 | 52 | //now we actually set the train objects to the newly calculated values 53 | train.all[locomotive].prototype.air.reservoir.main.psi.g = psig; 54 | train.all[locomotive].prototype.air.reservoir.main.psi.abs = psiAbs; 55 | //and then we update the gauge 56 | gauge.air.reservoir.main(psig) 57 | }, 58 | 59 | take : function(cfeet, atPressure, locomotive) { 60 | log.air("Taking " + cfeet + "@" + atPressure + "PSI from Main Reservoir on train.all[" + locomotive + "]"); 61 | //Define some shorthand variables 62 | var mainReservoir = {}; 63 | mainReservoir.psi = train.all[locomotive].prototype.reservoir.main.psi.g; 64 | mainReservoir.cap = train.all[locomotive].prototype.air.reservoir.main.capacity; 65 | mainReservoir.atmAirVol = train.all[locomotive].prototype.reservoir.main.currentAtmAirVolume; 66 | 67 | //using Boyle's law, figure out how much cubic feet @ the reservoir pressure is equal to cfeet @ atPressure 68 | var cfeetToTake = (cfeet * atPressure) / mainReservoir.psi; 69 | 70 | log.air("cfeetToTake = " + cfeetToTake); 71 | 72 | //subtract the amount of air (in cubic feet) we determined 73 | mainReservoir.newVol = mainReservoir.atmAirVol - cfeetToTake; 74 | 75 | log.air("New Main Reservoir Volume : " + mainReservoir.newVol); 76 | 77 | train.all[locomotive].prototype.reservoir.main.currentAtmAirVolume = mainReservoir.newVol; 78 | 79 | //update the gauge to reflect our changes 80 | if (locomotive == cab.current) { 81 | gauge.air.reservoir.main(train.all[locomotive].prototype.reservoir.main.psi.g); 82 | } 83 | 84 | //Return true or false based on whether or not it was able to do it 85 | if (mainReservoir.psi == 0) { 86 | return false; 87 | } 88 | else { 89 | return true; 90 | } 91 | }, 92 | 93 | //returns true or false whether an air device can operate with the current reservoir pressure 94 | pressureCheck : function(opsPressure, locomotive) { 95 | var resPressure = train.all[locomotive].prototype.reservoir.main.psi.g; 96 | var result = (resPressure >= opsPressure); 97 | if (result == false) {Materialize.toast("Not enough air pressure!", 2000);} 98 | return result; 99 | } 100 | } 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /scripts/brakes.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | /* 19 | This file is to keep the new braking code separate, at least until it's stable enough to move over into sim.js 20 | */ 21 | 22 | /*global brake:true, console*/ 23 | brake = { 24 | feedValvePSI: 90, //this seems to be the norm 25 | eqReservoirPSI: 90, //set both of these to the same thing ^^^ 26 | //changing the feed valve resets the brake system to fully charged and 0% braking 27 | charge : function () { 28 | log.Sim.brakes("Stopping sim to reset brake system..."); 29 | sim.stop(); //pause the sim while we do this to keep it from screwing with the physics 30 | for (var i=0; i < train.all.length; i++) { 31 | var car = train.all[i].prototype; 32 | //first set the equalizing reservoir 33 | brake.eqReservoirPSI = brake.feedValvePSI; //set the global equalizing reservoir first 34 | //set the brake line pressure to feedvalve pressure 35 | car.brake.linePSI = brake.feedValvePSI; 36 | //set the aux. reservoir psi 37 | car.brake.reservoirPSI = brake.feedValvePSI; 38 | car.brake.tripleValve = "R"; //set the triple valve to "release and charge" 39 | //set cylinder psi to 0 (meaning no brakes) 40 | car.brake.cylinderPSI = 0; 41 | //Run updated force calculation to reflect no braking pressure 42 | //THIS FUNCTIONALITY NOT IMPLEMENTED YET 43 | } 44 | log.Sim.brakes("Starting sim after brake reset..."); 45 | sim.start(100); //TODO, some kind of a setting 46 | log.Sim.brakes("Reset Brake System | Feed Valve: " + brake.feedValvePSI + "psi"); 47 | Materialize.toast("Reset Brake System | Feed Valve: " + brake.feedValvePSI + "psi", 3000); 48 | }, 49 | //finds the equalization pressure AND full service brake application for a brake pipe with feed valve set at ARG psi 50 | findEQpressure : function(psi) { 51 | /* 52 | Read more about equalization pressure here: http://alkrugsite.evilgeniustech.com/rrfacts/brakes.htm 53 | 54 | Algebra behind this function: 55 | Since a brake reservoir is 2.5 times the size of a brake cylinder, we can set up a problem to find the equalization pressure like this (for a 90psi brake pipe): 56 | 57 | 90 - x = 2.5x 58 | 59 | This can be rearranged to: 60 | 61 | 90 = 3.5x 62 | 63 | x is the full service brake REDUCTION. To find the equalizing pressure, subtract x from the pipe pressure. So for this, 64 | 65 | x = 26 66 | 67 | 90 - 26 = 64psi 68 | 69 | So for a 90psi brake pipe, you can only make up to a 26psi reduction. At that reduction, the brake pipe pressure, reservoir pressure, and cylinder pressure equalize, so you can't move any more air without releasing the brakes. 70 | 71 | This function is a programming implementation of that same math. 72 | */ 73 | var fullServiceReduction = Math.round(psi/3.5); 74 | var EQpressure = Math.round(psi - fullServiceReduction); 75 | 76 | var output = { 77 | EQpressure : EQpressure, 78 | fullServiceReduction : fullServiceReduction, 79 | } 80 | return output; 81 | }, 82 | //Send an emergency brake signal, which travels faster than the normal signals 83 | emergency : function() { 84 | 85 | }, 86 | //Called from sim.js with ARG car representing the element of the train to parse 87 | cycle : function(carNumber) { 88 | /* 89 | Braking Cycle 90 | -- Steps -- 91 | - Check the element before us's linePSI property, and see if theirs is different than ours 92 | - If no difference, do nothing. If difference, 93 | 94 | */ 95 | 96 | // IF this isn't the first car and it hasn't already been set up with a setTimeout 97 | if ((carNumber != 0) && (train.all[carNumber].prototype.brake.waitingOnChange == false)) { 98 | 99 | var frontNeighbor = train.all[(carNumber - 1)]; //represents the car in front of us (or locomotive in front of us) 100 | var car = train.all[carNumber]; //represents the car specified in the car argument 101 | //Check to see if frontNeighbor has a different pipe pressure than us 102 | //log.Sim.brakes("frontNeighbor number = " + (carNumber - 1)) 103 | if (frontNeighbor.prototype.brake.linePSI != car.prototype.brake.linePSI) { 104 | //if there is a pressure difference, setTimeout for when we should change this car's PSI 105 | var timeToWait = car.prototype.brake.latency; //the time it takes for the car to propagate the signal 106 | car.prototype.tmp.brakePSIchangeTimeout = setTimeout(function() { 107 | //code to run after the proper time has elapsed 108 | log.Sim.brakes("Changing linePSI on " + carNumber + " to " + frontNeighbor.prototype.brake.linePSI, 2000); 109 | car.prototype.brake.linePSI = frontNeighbor.prototype.brake.linePSI; 110 | car.prototype.brake.waitingOnChange = false; 111 | //now we change the triple valve state 112 | car.prototype.brake.tripleValveCycle(carNumber); 113 | }, timeToWait) 114 | car.prototype.brake.waitingOnChange = true; //this variable gets set to false once the psi finished propagating 115 | // WIP car.prototype.tmp.brakeApplicationInterval = setInterval(function()) 116 | } 117 | } 118 | else if (carNumber == 0) { 119 | //special version of this cycle for the leading element, which is always assumed to be a locomotive 120 | var eqReservoirPSI = brake.eqReservoirPSI; //find the psi of the equalizing reservoir 121 | var linePSI = train.all[0].prototype.brake.linePSI; //find train brake line PSI 122 | var waitingOnChange = train.all[0].prototype.brake.waitingOnChange; //this lets us know whether or not any differences in pressure have already been dealt with 123 | if ((eqReservoirPSI != linePSI) || (waitingOnChange == false)) { 124 | train.all[0].prototype.brake.linePSI = eqReservoirPSI; //Is this realistic enough? Not sure. 125 | } 126 | } 127 | }, 128 | //called by the train builder whenever a new car is added to fix the pressure on it 129 | fixNewElement : function(elNumber) { 130 | //pause the sim 131 | log.Sim.brakes("Pausing sim to set up new brakes on element " + elNumber + "...") 132 | sim.stop(); 133 | //set the reservoirPSI 134 | train.all[elNumber].prototype.brake.linePSI = brake.eqReservoirPSI //set to the equalizing reservoir PSI just to be easy and simple 135 | train.all[elNumber].prototype.brake.reservoirPSI = brake.eqReservoirPSI //same as above 136 | //cylinder psi should already be zero on a fresh element, so no need to set that 137 | log.Sim.brakes("Completed brake setup for element " + elNumber + ". Restarting sim..."); 138 | sim.start(); 139 | }, 140 | //when called, takes the average of all the localized brake pipe pressures and combines them into one average, which will show on the engineer's gauge. 141 | avgLinePSI : function() { 142 | var totalPSI = 0; 143 | for (var elNumber = 0; elNumber < train.all.length; elNumber++) { 144 | var linePSI = train.all[elNumber].prototype.brake.linePSI; 145 | totalPSI = totalPSI + linePSI; 146 | } 147 | var avg = totalPSI / train.all.length; //divide the sum of the pressures by the number of the cars 148 | return avg; 149 | }, 150 | } 151 | 152 | indBrake = { 153 | indValvePSI:0, //the PSI the independent brake valve wants it to be 154 | lastBailOffPSI:brake.feedValvePSI, //train brake pipe psi at the last time the bailoff button was pressed 155 | effectiveAutoBrakePSI:0, //how much the automatic brake has increased (if at all) since the last bail off 156 | effectiveIndPSI:0, //the actual PSI in the reference pipe, determined by favoring the independent or automatic brake valve 157 | maxPressure:undefined, //the maximum amount we can apply the ind. brake. See indBrake.calcMaxPressure() for more info 158 | bailOff: function(){ 159 | indBrake.lastBailOffPSI = brake.eqReservoirPSI; //remember the PSI we bail off at 160 | indBrake.calcEffIndPSI(); //run this to calculate the new pressure 161 | }, 162 | calcEffAutoBrakePSI: function() { 163 | //if the automatic brake has been released recently 164 | if ((indBrake.lastBailOffPSI - brake.eqReservoirPSI) <= 0) { 165 | //say the last time we bailed off was 70psi, then we released. 70-90=-20, but our effective autobrake PSI would be 0 166 | indBrake.effectiveAutoBrakePSI = 0; 167 | } 168 | //the normal math 169 | else { 170 | indBrake.effectiveAutoBrakePSI = indBrake.lastBailOffPSI - brake.eqReservoirPSI; //otherwise we calculate it the normal way 171 | // for example, last bailed off at 80, we drop to 70psi. 80psi - 70psi = 10psi effective brake pressure. 172 | } 173 | 174 | return indBrake.effectiveAutoBrakePSI; 175 | }, 176 | calcEffIndPSI: function() { 177 | //make sure the values we're about to use are up to date by calling this 178 | indBrake.calcEffAutoBrakePSI(); 179 | //figure out which brake pressure is greater and favor it 180 | var indBrakePSI = indBrake.indValvePSI; 181 | var autoBrakePSI = indBrake.effectiveAutoBrakePSI; 182 | if (indBrakePSI < autoBrakePSI) { 183 | log.Sim.brakes("indBrake: Favoring automatic brake for independent brake pressure; "+ autoBrakePSI + "PSI") 184 | indBrake.effectiveIndPSI = Number(autoBrakePSI); 185 | //now we let the user know a bailoff is possible 186 | ui.bailoff.set(true); 187 | } 188 | else { 189 | log.Sim.brakes("indBrake: Favoring independent brake valve for independent brake pressure; "+ indBrakePSI + "PSI") 190 | indBrake.effectiveIndPSI = indBrakePSI; 191 | //now we let the user know a bailoff will have no immediate effect since the ind. brake valve is favored 192 | ui.bailoff.set(false) 193 | } 194 | return indBrake.effectiveIndPSI; 195 | }, 196 | calcMaxPressure: function(){ 197 | //there's a 250% pressure increase in the automatic brake system. ie 10lb reduction = 25lb cylinder pressure 198 | var maxPressure = 2.5 * brake.findEQpressure(brake.feedValvePSI).fullServiceReduction; //we find the max pressure we can remove from the auto brake then multiply that by 2.5 to get the right pressure for this (since ind. brake is a straight air brake instead of the Westinghouse voodoo) 199 | indBrake.maxPressure = maxPressure; 200 | return maxPressure; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /scripts/buzz/buzz.min.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Buzz, a Javascript HTML5 Audio library 3 | // v1.2.0 - Built 2016-05-22 15:16 4 | // Licensed under the MIT license. 5 | // http://buzz.jaysalvat.com/ 6 | // ---------------------------------------------------------------------------- 7 | // Copyright (C) 2010-2016 Jay Salvat 8 | // http://jaysalvat.com/ 9 | // ---------------------------------------------------------------------------- 10 | 11 | !function(a,b){"use strict";"undefined"!=typeof module&&module.exports?module.exports=b():"function"==typeof define&&define.amd?define([],b):a.buzz=b()}(this,function(){"use strict";var a=window.AudioContext||window.webkitAudioContext,b={defaults:{autoplay:!1,crossOrigin:null,duration:5e3,formats:[],loop:!1,placeholder:"--",preload:"metadata",volume:80,webAudioApi:!1,document:window.document},types:{mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",aac:"audio/aac",m4a:"audio/x-m4a"},sounds:[],el:document.createElement("audio"),getAudioContext:function(){if(void 0===this.audioCtx)try{this.audioCtx=a?new a:null}catch(b){this.audioCtx=null}return this.audioCtx},sound:function(a,c){function d(a){for(var b=[],c=a.length-1,d=0;c>=d;d++)b.push({start:a.start(d),end:a.end(d)});return b}function e(a){return a.split(".").pop()}c=c||{};var f=c.document||b.defaults.document,g=0,h=[],i={},j=b.isSupported();if(this.load=function(){return j?(this.sound.load(),this):this},this.play=function(){return j?(this.sound.play(),this):this},this.togglePlay=function(){return j?(this.sound.paused?this.sound.play():this.sound.pause(),this):this},this.pause=function(){return j?(this.sound.pause(),this):this},this.isPaused=function(){return j?this.sound.paused:null},this.stop=function(){return j?(this.setTime(0),this.sound.pause(),this):this},this.isEnded=function(){return j?this.sound.ended:null},this.loop=function(){return j?(this.sound.loop="loop",this.bind("ended.buzzloop",function(){this.currentTime=0,this.play()}),this):this},this.unloop=function(){return j?(this.sound.removeAttribute("loop"),this.unbind("ended.buzzloop"),this):this},this.mute=function(){return j?(this.sound.muted=!0,this):this},this.unmute=function(){return j?(this.sound.muted=!1,this):this},this.toggleMute=function(){return j?(this.sound.muted=!this.sound.muted,this):this},this.isMuted=function(){return j?this.sound.muted:null},this.setVolume=function(a){return j?(0>a&&(a=0),a>100&&(a=100),this.volume=a,this.sound.volume=a/100,this):this},this.getVolume=function(){return j?this.volume:this},this.increaseVolume=function(a){return this.setVolume(this.volume+(a||1))},this.decreaseVolume=function(a){return this.setVolume(this.volume-(a||1))},this.setTime=function(a){if(!j)return this;var b=!0;return this.whenReady(function(){b===!0&&(b=!1,this.sound.currentTime=a)}),this},this.getTime=function(){if(!j)return null;var a=Math.round(100*this.sound.currentTime)/100;return isNaN(a)?b.defaults.placeholder:a},this.setPercent=function(a){return j?this.setTime(b.fromPercent(a,this.sound.duration)):this},this.getPercent=function(){if(!j)return null;var a=Math.round(b.toPercent(this.sound.currentTime,this.sound.duration));return isNaN(a)?b.defaults.placeholder:a},this.setSpeed=function(a){return j?(this.sound.playbackRate=a,this):this},this.getSpeed=function(){return j?this.sound.playbackRate:null},this.getDuration=function(){if(!j)return null;var a=Math.round(100*this.sound.duration)/100;return isNaN(a)?b.defaults.placeholder:a},this.getPlayed=function(){return j?d(this.sound.played):null},this.getBuffered=function(){return j?d(this.sound.buffered):null},this.getSeekable=function(){return j?d(this.sound.seekable):null},this.getErrorCode=function(){return j&&this.sound.error?this.sound.error.code:0},this.getErrorMessage=function(){if(!j)return null;switch(this.getErrorCode()){case 1:return"MEDIA_ERR_ABORTED";case 2:return"MEDIA_ERR_NETWORK";case 3:return"MEDIA_ERR_DECODE";case 4:return"MEDIA_ERR_SRC_NOT_SUPPORTED";default:return null}},this.getStateCode=function(){return j?this.sound.readyState:null},this.getStateMessage=function(){if(!j)return null;switch(this.getStateCode()){case 0:return"HAVE_NOTHING";case 1:return"HAVE_METADATA";case 2:return"HAVE_CURRENT_DATA";case 3:return"HAVE_FUTURE_DATA";case 4:return"HAVE_ENOUGH_DATA";default:return null}},this.getNetworkStateCode=function(){return j?this.sound.networkState:null},this.getNetworkStateMessage=function(){if(!j)return null;switch(this.getNetworkStateCode()){case 0:return"NETWORK_EMPTY";case 1:return"NETWORK_IDLE";case 2:return"NETWORK_LOADING";case 3:return"NETWORK_NO_SOURCE";default:return null}},this.set=function(a,b){return j?(this.sound[a]=b,this):this},this.get=function(a){return j?a?this.sound[a]:this.sound:null},this.bind=function(a,b){if(!j)return this;a=a.split(" ");for(var c=this,d=function(a){b.call(c,a)},e=0;eg&&i.volumea&&i.volume>a?(i.setVolume(i.volume-=1),e()):d instanceof Function&&d.apply(i)},h)}if(!j)return this;c instanceof Function?(d=c,c=b.defaults.duration):c=c||b.defaults.duration;var f,g=this.volume,h=c/Math.abs(g-a),i=this;return this.play(),this.whenReady(function(){e()}),this},this.fadeIn=function(a,b){return j?this.setVolume(0).fadeTo(100,a,b):this},this.fadeOut=function(a,b){return j?this.fadeTo(0,a,b):this},this.fadeWith=function(a,b){return j?(this.fadeOut(b,function(){this.stop()}),a.play().fadeIn(b),this):this},this.whenReady=function(a){if(!j)return null;var b=this;0===this.sound.readyState?this.bind("canplay.buzzwhenready",function(){a.call(b)}):a.call(b)},this.addSource=function(a){var c=this,d=f.createElement("source");return d.src=a,b.types[e(a)]&&(d.type=b.types[e(a)]),this.sound.appendChild(d),d.addEventListener("error",function(a){c.trigger("sourceerror",a)}),d},j&&a){for(var k in b.defaults)b.defaults.hasOwnProperty(k)&&void 0===c[k]&&(c[k]=b.defaults[k]);if(this.sound=f.createElement("audio"),null!==c.crossOrigin&&(this.sound.crossOrigin=c.crossOrigin),c.webAudioApi){var l=b.getAudioContext();l&&(this.source=l.createMediaElementSource(this.sound),this.source.connect(l.destination))}if(a instanceof Array)for(var m in a)a.hasOwnProperty(m)&&this.addSource(a[m]);else if(c.formats.length)for(var n in c.formats)c.formats.hasOwnProperty(n)&&this.addSource(a+"."+c.formats[n]);else this.addSource(a);c.loop&&this.loop(),c.autoplay&&(this.sound.autoplay="autoplay"),c.preload===!0?this.sound.preload="auto":c.preload===!1?this.sound.preload="none":this.sound.preload=c.preload,this.setVolume(c.volume),b.sounds.push(this)}},group:function(a){function b(){for(var b=c(null,arguments),d=b.shift(),e=0;e=10?c:"0"+c,d=b?Math.floor(a/60%60):Math.floor(a/60),d=isNaN(d)?"--":d>=10?d:"0"+d,e=Math.floor(a%60),e=isNaN(e)?"--":e>=10?e:"0"+e,b?c+":"+d+":"+e:d+":"+e},fromTimer:function(a){var b=a.toString().split(":");return b&&3===b.length&&(a=3600*parseInt(b[0],10)+60*parseInt(b[1],10)+parseInt(b[2],10)),b&&2===b.length&&(a=60*parseInt(b[0],10)+parseInt(b[1],10)),a},toPercent:function(a,b,c){var d=Math.pow(10,c||0);return Math.round(100*a/b*d)/d},fromPercent:function(a,b,c){var d=Math.pow(10,c||0);return Math.round(b/100*a*d)/d}};return b}); -------------------------------------------------------------------------------- /scripts/github.js: -------------------------------------------------------------------------------- 1 | gh = { 2 | update: function (user, repo) { 3 | gh.releases.update(user, repo) 4 | gh.tags.update(user, repo) 5 | setTimeout(gh.processReleaseInfo, 500); 6 | }, 7 | processReleaseInfo: function () { 8 | $.get(bundles.tools.getBaseUrl() + ".git/refs/heads/master", function (data) { 9 | gh.localVersion.commit = data.substring(0, data.length - 1); 10 | var releases = gh.releases.listByCommit(); 11 | for (var x = 0; x < releases.length; x++) { 12 | if (releases[x].commit.sha == gh.localVersion.commit) { 13 | gh.localVersion.release = releases[x].name; 14 | gh.localVersion.prerelease = releases[x].prerelease; 15 | gh.localVersion.releaseNotes = releases[x].releaseNotes; 16 | gh.localVersion.publishedAt = releases[x].publishedAt; 17 | } 18 | } 19 | if (gh.localVersion.release == "") { 20 | gh.localVersion.release = "rolling-" + gh.localVersion.commit; 21 | gh.localVersion.prerelease = true; 22 | gh.localVersion.releaseNotes = "https://github.com/k4kfh/ZephyrCab/commit/" + gh.localVersion.commit; 23 | gh.localVersion.publishedAt = null; 24 | } 25 | 26 | $("#github-release-name").html("" + gh.localVersion.release + "") 27 | if (gh.localVersion.releaseNotes != "") { 28 | $("#github-release-name").attr('href', gh.localVersion.releaseNotes); 29 | } 30 | if (gh.localVersion.prerelease) { 31 | $("#github-release-prerelease").html("Prerelease (Unstable)") 32 | } else { 33 | $("#github-release-prerelease").html("Regular Release") 34 | } 35 | 36 | //is this the most up to date release 37 | var dates = [] 38 | gh.releases.listByCommit().forEach(function (rel) { 39 | dates.push(rel.publishedAt) 40 | }) 41 | var latestDate = Math.max.apply(null, dates) 42 | var latestRelease; //define for scoping 43 | gh.releases.listByCommit().forEach(function(rel){ 44 | if (rel.publishedAt.getTime() == latestDate) { 45 | latestRelease = rel; 46 | } 47 | }) 48 | 49 | //if it's not a rolling release (where there is no applicable date) and it's not the latest release, let the user know 50 | if (gh.localVersion.release.indexOf("rolling") != -1) { 51 | $("#update-available").html("Rolling releases require manual update checks.") 52 | } 53 | else if (gh.localVersion.publishedAt.getTime() != latestDate && gh.localVersion.release.indexOf("rolling") == -1) { 54 | $("#update-available").html("Update available!") 55 | $("#update-available").attr("href", latestRelease.releaseNotes) 56 | //only alert the user actively if the release is stable 57 | if (latestRelease.prerelease == false) { 58 | alert("New stable release available! Download the update at " + latestRelease.releaseNotes) 59 | } 60 | } 61 | else { 62 | $("#update-available").html("No updates available.") 63 | } 64 | }); 65 | }, 66 | releases: { 67 | update: function (user, repository) { 68 | $.get("https://api.github.com/repos/" + user + "/" + repository + "/releases", function (data, status) { 69 | gh.releases.rawdata = data; 70 | }); 71 | }, 72 | listByCommit: function () { 73 | var output = [] 74 | gh.releases.rawdata.forEach(function (rel) { 75 | gh.tags.rawdata.forEach(function (tag) { 76 | if (tag.name == rel.tag_name) { 77 | var object = { 78 | "name": tag.name, 79 | "commit": tag.commit, 80 | "releaseNotes": rel.html_url, 81 | "prerelease": rel.prerelease, 82 | "publishedAt": new Date(rel.published_at), 83 | } 84 | output.push(object) 85 | } 86 | }) 87 | }) 88 | return output; 89 | }, 90 | rawdata: [], 91 | }, 92 | tags: { 93 | update: function (user, repository) { 94 | $.get("https://api.github.com/repos/" + user + "/" + repository + "/tags", function (data, status) { 95 | gh.tags.rawdata = data; 96 | }); 97 | }, 98 | rawdata: "", 99 | }, 100 | localVersion: { 101 | commit: "", 102 | release: "", 103 | prerelease: "", 104 | releaseNotes: "", 105 | } 106 | } 107 | 108 | gh.update("k4kfh", "ZephyrCab") 109 | -------------------------------------------------------------------------------- /scripts/jmri-core.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | /* 19 | JMRI CORE 20 | 21 | This file contains what amounts to a scratchbuilt JMRI interface. I opted to build my own interface for greater flexibility than using the provided JMRI jQuery plugin, which at the time of this file's creation I did not know about. 22 | 23 | This is not the lowest level portion of the interface, however. websockets.js contains most of the direct interface, and handles things like the layout connection initiation/termination. This file does not deal directly with the network; it is responsible for relaying info between the rest of ZephyrCab and the JMRI JSON servlet. 24 | */ 25 | 26 | var jmri = {}; 27 | 28 | /* 29 | you can call this with keyword new to create a new throttle object that has all the functions of a working throttle. no decoder-specific anything, just like it would be on a Digitrax throttle or something 30 | 31 | example: exampleThrottle = new jmri.throttle(1379, 0) 32 | now I can run exampleThrottle.f.set(0, true) to turn on #1379's headlight. You get the idea. 33 | 34 | This function should not be used by anything except the core train builder stuff; if you try and build your own throttle stuff using this it WILL BREAK THINGS! 35 | */ 36 | jmri.throttle = function(address, throttleName) { 37 | if (link.status === true) { 38 | //this second if statement makes sure we have our decoder.js script loaded, because this is super duper important and yeah 39 | link.send('{"type":"throttle","data":{"throttle":"' + throttleName + '","address":' + address + '}}') 40 | log.jmri("Requested throttle " + throttleName + " for locomotive #" + address) 41 | this.address = address 42 | this.name = throttleName; //throttle name should always be the train position just for ease-of-development purposes 43 | this.speed = {}; //same reason as this.f for existing as a seemingly stupid object 44 | this.direction = {}; 45 | //called when removing object from the train; it releases the throttle 46 | this.release = function() { 47 | releasecmd = '{"type":"throttle","data":{"throttle":"' + throttleName + '","release":null}}'; 48 | link.send(releasecmd); 49 | debugToast("Sent command : " + releasecmd) 50 | } 51 | this.speed.set = function(speed) { 52 | //set speed to given percent 53 | link.send('{"type":"throttle","data":{"address":' + address + ', "throttle":"' + throttleName + '", "speed":' + speed + '}}') 54 | } 55 | 56 | //Takes a 1 or a -1 as an argument 57 | this.direction.set = function(direction) { 58 | var forwardOrNot = (direction === 1); //create a boolean that's true when forward/false when reverse 59 | link.send('{"type":"throttle","data":{"address":' + address + ', "throttle":"' + throttleName + '", "forward":' + forwardOrNot + '}}') 60 | } 61 | this.f = new Object(); //the reason we did this as an object with only one function was to leave room for future ability to store the states of the functions. I will add it if I need it, but its a pain so I haven't yet. 62 | this.f.set = function(inputData) { 63 | var finalCommand = [] 64 | //This long train of IF statements will set each function 65 | for (i=0; i <= 28; i++) { 66 | var valueName = "F" + i; 67 | 68 | //check to see if we got a definition for this function from the user 69 | if (inputData[valueName]!=undefined) { 70 | var segment = ('"' + valueName + '" : ' + inputData[valueName]); 71 | finalCommand.push(segment); 72 | } 73 | } 74 | 75 | var finalString = finalCommand.join(", "); 76 | link.send('{"type":"throttle","data":{"address":' + address + ', "throttle":"' + throttleName + '",' + finalString + '}}') 77 | } 78 | 79 | //TODO - add command here so that when a throttle is acquired, all functions are set to off and the speed to 0 80 | } 81 | else { 82 | Materialize.toast("You need to set up your WebSockets connection first!", 4000) 83 | } 84 | 85 | } 86 | 87 | 88 | //call with state as boolean 89 | jmri.trkpower = function(option) { 90 | if (option == true) { 91 | link.send('{"type":"power","data":{"state":2}}') 92 | log.jmri("Track power set to ON") 93 | } 94 | else if (option == false) { 95 | link.send('{"type":"power","data":{"state":4}}') 96 | log.jmri("Track power set to OFF") 97 | } 98 | 99 | else if (option == "toggle") { 100 | //if track power is currently on, turn it off 101 | if (layoutTrackPower_state == true) { 102 | link.send('{"type":"power","data":{"state":4}}') 103 | log.jmri("Track power set to OFF") 104 | } 105 | //if its currently off, turn it on 106 | else if (layoutTrackPower_state == false) { 107 | link.send('{"type":"power","data":{"state":2}}') 108 | log.jmri("Track power set to ON") 109 | } 110 | } 111 | } 112 | 113 | jmri.railroadName = "Railroad" //this is set upon connection 114 | jmri.hellomsg //initial railroad hello message 115 | 116 | jmri.handleType = new Object(); //this contains all the non-locomotive/throttle related handler functions 117 | jmri.handleType.power = function(string) { 118 | var json = string 119 | if (json.data.state == 2) { 120 | jmri.trkpower.state = true; 121 | log.jmri("Updated layout track power status to TRUE"); 122 | $("#track-power").prop("checked", true); 123 | } 124 | else if (json.data.state == 4) { 125 | jmri.trkpower.state = false; 126 | log.jmri("Updated layout track power status to FALSE"); 127 | $("#track-power").prop("checked", false); 128 | } 129 | } 130 | 131 | jmri.roster = new Object(); 132 | 133 | 134 | /* 135 | This is a special version of the JMRI roster. 136 | 137 | The returned value from the JMRI JSON server when you request the roster is the in the form of an array of objects. You cannot look up objects by their name, or by any other property, you can only request their number in the array. This variable is automatically generated as an object with the entry names as keys. The values of these keys are the data attributes of the raw roster. This means you can look up a locomotive by name, and it is part of what helps jmri.roster.matchProperty() work. 138 | */ 139 | jmri.roster.entries = new Object(); 140 | 141 | 142 | /* 143 | This contains the roster, raw, as returned by the JSON servlet. It auto-updates if we recieve any new data at any time. 144 | */ 145 | jmri.roster.raw = new Object(); 146 | 147 | 148 | /* 149 | This function is not to be used by any front-end scripts. This is only called by websockets.js when it recieves updated roster data. 150 | 151 | Because of this, jmri.roster.entries is ALWAYS up-to-date with whatever data is in jmri.roster.raw. 152 | */ 153 | jmri.roster.reformat = function(rosterRaw) { 154 | var newRoster = new Object(); 155 | for (i = 0; i < rosterRaw.length; i++) { 156 | //run for each element of the raw roster 157 | var entry = rosterRaw[i]; 158 | var name = entry.data.name 159 | newRoster[name] = entry.data 160 | } 161 | return newRoster; 162 | } 163 | 164 | 165 | /* 166 | This function is used to find entries in the JMRI roster which match a certain object. 167 | 168 | You call it with: 169 | jmri.roster.matchProperty({"property":"value"}) 170 | 171 | The function returns the name keys of all the entries that have the property with the correct value. 172 | 173 | For example, jmri.roster.matchProperty({"decoderFamily":"fakeDecoderFamily"}) would return an array of the names of every locomotive whose decoderFamily attribute equals "fakeDecoderFamily". If nothing fits the query, it will return an empty array, or []. 174 | */ 175 | jmri.roster.matchProperty = function(property) { 176 | var rosterEntries = Object.keys(jmri.roster.entries) //get an array of all the locomotive names for easy for looping 177 | var results = [] 178 | for (i = 0; i < rosterEntries.length; i++) { 179 | //this code runs for each roster entry 180 | var entryName = rosterEntries[i]; 181 | var key = (Object.keys(property))[0] //we always use the first element in the keys list. this is just some idiot proofing 182 | var value = property[key] 183 | if (jmri.roster.entries[entryName][key] == value) { 184 | results.push(entryName) 185 | } 186 | } 187 | return results; 188 | } 189 | jmri.throttleName = new Object(); 190 | jmri.throttleName.object = 0 191 | jmri.throttleName.generate = function() { 192 | jmri.throttleName.object++ 193 | return jmri.throttleName.object; 194 | } 195 | 196 | -------------------------------------------------------------------------------- /scripts/pretty-logs.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | //This is simply a wrapper to add easy filter keywords to the logs 19 | log = { 20 | jmri : function(string) { 21 | console.log("JMRI: " + string) 22 | }, 23 | websockets : function(string) { 24 | console.log("WEBSOCKETS: " + string) 25 | }, 26 | bundles : function(string) { 27 | console.log("BUNDLES: " + string) 28 | }, 29 | Bundles: { 30 | generator : function(string){ 31 | console.log("BUNDLES.GENERATOR: " + string) 32 | } 33 | }, 34 | trainbuilder: function(string) { 35 | console.log("TRAINBUILDER: " + string) 36 | }, 37 | ui : function(string) { 38 | console.log("UI: " + string) 39 | }, 40 | Ui : { 41 | gauges : function(string){ 42 | console.log("UI.GAUGES: " + string) 43 | }, 44 | input : function(string) { 45 | console.log("UI.INPUT: " + string) 46 | } 47 | }, 48 | sim : function(string) { 49 | console.log("SIM: " + string) 50 | }, 51 | //in addition to the generic catch-all, there are also subcategories 52 | Sim : { 53 | wheelslip : function(string){ 54 | console.log("SIM.WHEELSLIP: " + string) 55 | }, 56 | brakes: function(string){ 57 | console.log("SIM.BRAKES: " + string) 58 | }, 59 | tractiveeffort: function(string){ 60 | console.log("SIM.TRACTIVEEFFORT: " + string) 61 | }, 62 | air: function(string){ 63 | console.log("SIM.AIR: " + string) 64 | } 65 | }, 66 | decoder : function(string){ 67 | console.log("DECODER: "+string) 68 | }, 69 | stats : function(string){ 70 | console.log("STATS: " + string) 71 | } 72 | 73 | } 74 | 75 | //dump an initial log message to the console for debugging info 76 | console.info("-----------------------------------------------------") 77 | console.info(" ______ _ _____ _ ") 78 | console.info(" |___ / | | / ____| | | ") 79 | console.info(" / / ___ _ __ | |__ _ _ _ __| | __ _| |__ ") 80 | console.info(" / / / _ \ '_ \| '_ \| | | | '__| | / _` | '_ \ ") 81 | console.info(" / /_| __/ |_) | | | | |_| | | | |___| (_| | |_) |") 82 | console.info(" /_____\___| .__/|_| |_|\__, |_| \_____\__,_|_.__/ ") 83 | console.info(" | | __/ | ") 84 | console.info(" |_| |___/ ") 85 | console.info("-----------------------------------------------------") 86 | console.info("LOGGING KEYWORDS:") 87 | console.info("• JMRI") 88 | console.info("• WEBSOCKETS") 89 | console.info("• BUNDLES") 90 | console.info(" • BUNDLES.GENERATOR") 91 | console.info("• TRAINBUILDER") 92 | console.info("• UI") 93 | console.info(" • UI.GAUGES") 94 | console.info(" • UI.INPUT") 95 | console.info("• SIM") 96 | console.info(" • SIM.WHEELSLIP") 97 | console.info(" • SIM.AIR") 98 | console.info(" • SIM.BRAKES") 99 | console.info(" • SIM.TRACTIVEEFFORT") 100 | console.info("• DECODER") 101 | console.info("• STATS") 102 | console.info("-----------------------------------------------------") 103 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | util = { 19 | arrayToDropdown: function (options, selector) { 20 | $(selector).empty(); 21 | $(selector).append('') 22 | $.each(options, function (i, p) { 23 | $(selector).append($('').val(p).html(p)); 24 | }) 25 | } 26 | } 27 | 28 | setup = { 29 | load: function (loconame) { 30 | alert("ya boi") 31 | }, 32 | compare: { 33 | getUnconfiguredLocomotives: function () { 34 | var rosterList = Object.keys(jmri.roster.entries); 35 | var bundlesList = Object.keys(bundles.locomotives); 36 | var unbundled = []; 37 | for (entry in rosterList) { 38 | if (bundlesList.indexOf(rosterList[entry]) == -1) { 39 | //entry has no bundle 40 | unbundled.push(rosterList[entry]); 41 | } 42 | } 43 | return unbundled; 44 | } 45 | }, 46 | onConnect: function () { //called when we connect to JMRI 47 | //setup page stuff 48 | util.arrayToDropdown(setup.compare.getUnconfiguredLocomotives(), "#setup-loco-select") //populate dropdown with unbundled locomotives 49 | $("#setup-num-unbundled").html(setup.compare.getUnconfiguredLocomotives().length) //set how many unbundled locomotives you have 50 | $("#setup-form").find("input").change(function () { 51 | setup.generate(); 52 | }) 53 | }, 54 | generate: function () { 55 | //regenerate the bundle text below 56 | var generatedBundle = { 57 | type: "locomotive", 58 | prototype: { 59 | "builder": $("#setupForm-builder").val(), 60 | "name": $("#setupForm-name").val(), 61 | "weight": Number($("#setupForm-weight").val()), //Weight of the locomotive in lbs 62 | "maxHP": Number($("#setupForm-maxHP").val()), //Horsepower of the locomotive 63 | "maxAmps": Number($("#setupForm-maxAmps").val()), //Max current of the locomotive 64 | "notchRPM": [ 65 | Number($("#setupForm-notch0-rpm").val()), 66 | Number($("#setupForm-notch1-rpm").val()), 67 | Number($("#setupForm-notch2-rpm").val()), 68 | Number($("#setupForm-notch3-rpm").val()), 69 | Number($("#setupForm-notch4-rpm").val()), 70 | Number($("#setupForm-notch5-rpm").val()), 71 | Number($("#setupForm-notch6-rpm").val()), 72 | Number($("#setupForm-notch7-rpm").val()), 73 | Number($("#setupForm-notch8-rpm").val()), 74 | ], 75 | "notchMaxSpeeds": [ 76 | null, 77 | Number($("#setupForm-notch1-maxSpeed").val()), 78 | Number($("#setupForm-notch2-maxSpeed").val()), 79 | Number($("#setupForm-notch3-maxSpeed").val()), 80 | Number($("#setupForm-notch4-maxSpeed").val()), 81 | Number($("#setupForm-notch5-maxSpeed").val()), 82 | Number($("#setupForm-notch6-maxSpeed").val()), 83 | Number($("#setupForm-notch7-maxSpeed").val()), 84 | Number($("#setupForm-notch8-maxSpeed").val()), 85 | ], 86 | "engineRunning": 0, //0 or 1 - 1 is on, 0 is off 87 | "startingTE": Number($("#setupForm-startingTE").val()), 88 | "drivetrainEfficiency": Number($("#setupForm-drivetrainEfficiency").val()), 89 | scaleSpeedCoefficient: Number($("#setupForm-scaleSpeedCoefficient").val()), 90 | 91 | wheelSlip: { 92 | adhesion: Number($("#setupForm-adhesion").val()), //adhesion factor (in percent) 93 | adhesionDuringSlip: Number($("#setupForm-adhesionDuringSlip").val()), //adhesion factor for slipping wheels 94 | }, 95 | 96 | air: //holds static and realtime data about pneumatics 97 | { 98 | reservoir: { 99 | main: { 100 | capacity: Number($("#setupForm-mainReservoirCapacity").val()), //capacity of the tank in cubic feet 101 | leakRate: Number($("#setupForm-mainReservoirLeakRate").val()), //leak rate in cubic feet per 100ms 102 | }, 103 | }, 104 | compressor: { 105 | //STATIC DATA 106 | limits: { 107 | lower: Number($("#setupForm-compressorLowerLimit").val()), //This is the point at which the compressor will turn back on and fill up the air reservoir (psi) 108 | upper: Number($("#setupForm-compressorUpperLimit").val()), //This is the point at which the compressor will turn off (psi) 109 | }, 110 | flowrateCoeff: Number($("#setupForm-flowRateCoeff").val()), //This is cfm/rpm, derived from "255cfm @ 900rpm for an SD45" according to Mr. Al Krug 111 | }, 112 | }, 113 | 114 | "coeff": { 115 | rollingResistance:Number($("#setupForm-rollingResistance").val()), 116 | }, 117 | 118 | brake: { 119 | //air brake equipment information 120 | latency: Number($("#setupForm-brakeLatency").val()), //time it takes to propagate a signal through the car, in milliseconds 121 | }, 122 | }, 123 | }; 124 | var locoName = $('#setup-loco-select').find(":selected").text(); 125 | 126 | var finishedBundle = 'tmp = {"' + locoName + '" : ' +JSON.stringify(generatedBundle, null, 4) + "};"; 127 | 128 | var dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(finishedBundle); 129 | var linkElement = document.getElementById("setup-download-bundle"); 130 | //if the user forgot to select a locomotive for the bundle to apply to 131 | if (locoName == "Choose a locomotive to configure.") { 132 | linkElement.setAttribute('onclick', "alert('You need to choose a locomotive from your JMRI roster!')") 133 | } 134 | else { 135 | linkElement.setAttribute('onclick', undefined) 136 | linkElement.setAttribute('href', dataUri); 137 | var filename = locoName; 138 | filename = filename.replace(" ", ""); //strip out spaces 139 | filename = filename.replace(/[^a-zA-Z ]/g, ""); //strip out special chars 140 | linkElement.setAttribute('download', filename+".zephyrcab") 141 | } 142 | return generatedBundle; 143 | 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /scripts/sim.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | //This is a really ugly temporary file to hold a quick rough draft of a new sim.accel function 19 | sim = {}; 20 | 21 | /* 22 | Notching Objects 23 | 24 | notch is the top level object for all this, and it contains "state" and "set()" which are pretty self explanatory 25 | 26 | set() is called with a number as it's only argument. If a user tries to raise the notch by more than one, then the function will return the existing notch. The function will always return the actual notch. 27 | */ 28 | notch = new Object(); 29 | notch.state = 0 //Notch defaults to 0, which is idling. 30 | notch.set = function(newNotch) { 31 | //This function used to limit notch changes to 1 click at a time, but that got to be cumbersome on a touch screen. So it's basically a dummy function now, I just would rather have the abstraction layer for notch.state leftover if I decide I need it for something. Because ain't nobody got time to do massive codebase changes. 32 | notch.state = newNotch; 33 | return notch.state; 34 | } 35 | 36 | //We also go ahead and define the reverser so the math works 37 | reverser = 0; //NEUTRAL not FWD 38 | 39 | sim = new Object(); 40 | sim.direction = 1 //1 means forward, -1 means reverse, 0 means we're currently stopped. This is the ACTUAL direction, which is not necessarily the reverser's direction. 41 | sim.time = { 42 | speed: 1, 43 | } 44 | 45 | //We need to go ahead and define all the stuff inside train.total and set it to 0 46 | train.total = new Object(); 47 | train.total = { 48 | netForce: 0, 49 | weight: 0, 50 | accel: { 51 | si: { 52 | force: 0, 53 | mass: 0, 54 | acceleration: 0 55 | }, 56 | acceleration_mph: 0, 57 | speed: { 58 | mph: 0, 59 | ms: 0, 60 | } 61 | } 62 | } 63 | 64 | sim.accel = function() { 65 | //make sure the train actually has elements 66 | if (train.all.length !== 0) { 67 | 68 | //We have to clear these variables on start, otherwise the train will progressively get heavier as we walk the array a few times. We do this the ugly way instead of the json way because doing it the json way also resets the speed to zero. 69 | train.total.netForce = 0; 70 | train.total.weight = 0; 71 | train.total.accel.si.force = 0; 72 | train.total.accel.si.mass = 0; 73 | train.total.accel.si.acceleration = 0; 74 | train.total.accel.acceleration_mph = 0; 75 | 76 | //Now we increment over every train object 77 | for (var i = 0; i < train.all.length; i++) { 78 | 79 | if (train.all[i].type == 'locomotive') { 80 | //Locomotive Specific Stuff 81 | 82 | /* 83 | If the engine is running: 84 | - Calculate RPM and tractive effort 85 | - Figure out notching sounds on applicable decoders 86 | - Start or stop the air compressor depending on reservoir pressure 87 | - Calculate amperage 88 | 89 | If the engine is not running (the else statement): 90 | - Set RPM and Tractive Effort to 0 91 | - Don't do anything with notching sounds 92 | - Stop the air compressor 93 | */ 94 | if (train.all[i].prototype.engineRunning === 1) { 95 | //Calculates the engine RPM, which is necessary for compressor flow rate 96 | train.all[i].prototype.realtime.rpm = train.all[i].prototype.engineRunning * train.all[i].prototype.notchRPM[notch.state]; 97 | //SETTING NOTCHING SOUNDS 98 | if (train.all[i].dcc.f.notch.state != notch.state) { 99 | //We know it's changed, now we have to figure out which direction (up or down) to move it. 100 | var difference = notch.state - train.all[i].dcc.f.notch.state; //This will equal 1 or -1, telling us the direction to notch 101 | //console.log("Difference in notch: " + difference) 102 | if (difference == 1) { 103 | train.all[i].dcc.f.notch.up(); 104 | } else if (difference == -1) { 105 | train.all[i].dcc.f.notch.down(); 106 | } 107 | } else { 108 | //console.log("No notch difference found") 109 | } 110 | 111 | //call the tractive effort calculation function of the locomotive's bundle 112 | train.all[i].prototype.realtime.teIgnoreSlip = train.all[i].prototype.calc.te(train.total.accel.speed.mph, i); //this is before we take slip into consideration 113 | 114 | //Now that we've calculated TE, we calculate amps! 115 | train.all[i].prototype.calc.amps(i); 116 | gauge.amps(train.all[i].prototype.realtime.amps) 117 | /* 118 | ROLLING RESISTANCE AND GENERAL DRAG 119 | 120 | This is where rolling resistance, along with a general drag coefficient (WIP!) to account for bearings and the like, is calculated. 121 | */ 122 | train.all[i].prototype.realtime.rollingResistance = sim.direction * -1 * train.all[i].prototype.coeff.rollingResistance * train.all[i].prototype.weight 123 | //This IF statement makes sure we dont accidentally have it pull the train backwards if it's sitting still. 124 | if (train.total.accel.speed.mph == 0) { 125 | train.all[i].prototype.realtime.rollingResistance = 0 126 | } 127 | 128 | /* 129 | COMPRESSOR AND AIR RESERVOIR(S) 130 | 131 | This is calculated using an algebraically twisted version of Boyle's law. We basically find how much volume (at atmosphere pressure) has been crammed into a fixed space, so instead of decreasing volume and keeping mass of air the same, we are increasing mass of air and keeping tank volume constant. 132 | 133 | Steps: 134 | 1. See if dump valve is open 135 | 2. Turn compressor on or off based on current pressure 136 | 3. Find compressor output flow rate (in cubic feet per physics cycle). 137 | 4. Find volume of atmosphere-pressure air that is in the tank. 138 | 5. Account for the steady leak rate specified in the prototype file. 139 | 140 | More information on all this to come. 141 | */ 142 | //Define some shorthand variables for readability 143 | var compressor = train.all[i].prototype.air.compressor, 144 | dumpValve = train.all[i].prototype.air.reservoir.main.dump, 145 | upperLimit = train.all[i].prototype.air.compressor.limits.upper, 146 | lowerLimit = train.all[i].prototype.air.compressor.limits.lower, 147 | psi = train.all[i].prototype.air.reservoir.main.psi.g, 148 | cfmRpmRatio = train.all[i].prototype.air.compressor.flowrateCoeff, //ratio of cfm per rpm 149 | rpm = train.all[i].prototype.realtime.rpm; 150 | /* 151 | IF/ELSE Tasks 152 | 1. See if dump valve is open 153 | 2. Turn compressor on or off based on current pressure 154 | */ 155 | if (dumpValve == false) { 156 | //If pressure is too low, start compressor 157 | if (psi < lowerLimit) { 158 | //Turn on compressor 159 | compressor.running = 1; 160 | train.all[i].dcc.f.compressor.set(true); 161 | } 162 | //If pressure is too high, stop compressor 163 | else if (psi > upperLimit) { 164 | //Turn off compressor 165 | compressor.running = 0; 166 | train.all[i].dcc.f.compressor.set(false); 167 | //Set flow rate to 0cfm 168 | 169 | } 170 | } 171 | //If dump valve is open, make sure compressor is off 172 | else { 173 | compressor.running = 0; //we set this variable for the functions in air.js to use 174 | } 175 | 176 | /* 177 | TASKS 178 | 3. Find compressor output flow rate (in cubic feet per physics cycle). 179 | */ 180 | var flowratePerCycle = ((rpm * cfmRpmRatio) / 600) * compressor.running; //we divide this by 600 to change it from cubic feet per minute to cubic feet per 100ms (since sim.js recalculates every 100ms). Store this locally only since we won't need it again. Also note that it's multiplied by compressor.running to nullify it when the compressor is off 181 | 182 | //Add flowrate (in cubic feet per cycle) to the airVolumeInTank variable. 183 | //This huge long statement really just says (currentAtmAirVolume = currentAtmAirVolume + flowratePerCycle) 184 | train.all[i].prototype.air.reservoir.main.currentAtmAirVolume = train.all[i].prototype.air.reservoir.main.currentAtmAirVolume + flowratePerCycle; 185 | log.Sim.air("FLOW RATE PER CYCLE FOR i=" + i + " IS " + flowratePerCycle) 186 | 187 | //Subtract leak rate in cubic feet before calculating pressure 188 | var volumeInTank = train.all[i].prototype.air.reservoir.main.currentAtmAirVolume; 189 | var leakRate = train.all[i].prototype.air.reservoir.main.leakRate; //this is loss in cubic feet per cycle 190 | //The business end of this messy code here 191 | var volumeInTank = volumeInTank - leakRate; 192 | //More jostling variables around 193 | train.all[i].prototype.air.reservoir.main.currentAtmAirVolume = volumeInTank; 194 | 195 | //Make sure the volume isn't below the capacity of the reservoir (otherwise we'll have a vacuum) 196 | if (train.all[i].prototype.air.reservoir.main.currentAtmAirVolume < train.all[i].prototype.air.reservoir.main.capacity) { 197 | //if the volume is less than the minimum (the capacity) then fix it 198 | train.all[i].prototype.air.reservoir.main.airVolumeInTank = train.all[i].prototype.air.reservoir.main.capacity; 199 | } 200 | air.reservoir.main.updatePSI(i) //this takes all those numbers we just figured out and calculates the PSI, then updates the gauge 201 | } else { 202 | /* 203 | If the engine is NOT running: 204 | - Set RPM and Tractive Effort to 0 205 | - Don't do anything with notching sounds 206 | - Set fuel consumption to 0 207 | - Stop the air compressor 208 | - Still find the PSI of the main reservoir and the brake cylinder and whatnot 209 | */ 210 | train.all[i].prototype.realtime.rpm = 0; 211 | train.all[i].prototype.realtime.te = 0; 212 | //Turn off air compressor sound 213 | train.all[i].dcc.f.compressor.set(false); 214 | //Turn off air compressor simulation 215 | train.all[i].prototype.air.compressor.running = 0; 216 | //update PSI for main reservoir based on the numbers we have, just so the number is still there 217 | air.reservoir.main.updatePSI(i) 218 | //set amps to zero 219 | train.all[i].prototype.realtime.amps = 0; 220 | gauge.amps(train.all[i].prototype.realtime.amps) 221 | } 222 | 223 | //BRAKES 224 | //If the auto brakes are released we make sure to call this so bail-off behaves 225 | if (brake.eqReservoirPSI == brake.feedValvePSI) { 226 | indBrake.bailOff(); 227 | } 228 | //This calculates the new pressure for the independent brake system 229 | indBrake.calcEffIndPSI(); 230 | //calculate the cylinder pressure (responsibility of relay valve) and the braking force of your locomotive 231 | train.all[i].prototype.brake.ind.calcForce(i, indBrake.effectiveIndPSI); 232 | 233 | //WHEEL SLIP 234 | //figure out if we're slipping or not 235 | var slipping = train.all[i].prototype.wheelSlip.slipCalc(i) 236 | ui.wheelSlip.set(slipping) 237 | if (slipping) { 238 | train.all[i].prototype.realtime.te = 0; //there's no TE if we're slipping 239 | } 240 | else { 241 | train.all[i].prototype.realtime.te = train.all[i].prototype.realtime.teIgnoreSlip; //if we're not slipping, just pass the number through 242 | } 243 | //Find/store net force BEFORE factoring in slip (for the slip calculation) 244 | train.all[i].prototype.realtime.netForceIgnoreSlip = train.all[i].prototype.realtime.teIgnoreSlip + train.all[i].prototype.realtime.rollingResistance + train.all[i].prototype.brake.brakingForce; 245 | //Now we find/store the net force for the locomotive, factoring in slip 246 | train.all[i].prototype.realtime.netForce = train.all[i].prototype.realtime.te + train.all[i].prototype.realtime.rollingResistance + train.all[i].prototype.brake.brakingForce; 247 | /*Locomotive-Only Totaling Math 248 | Steps: 249 | 1. Add weight to total weight 250 | 2. Add tractive effort to total net force 251 | 3. Add braking force to total braking force (TODO) 252 | */ 253 | train.total.weight = train.total.weight + train.all[i].prototype.weight; //weight = weight + element.weight 254 | train.total.netForce = train.total.netForce + train.all[i].prototype.realtime.netForce; 255 | 256 | } 257 | if (train.all[i].type == "rollingstock") { 258 | //Rolling Stock Specific Stuff 259 | 260 | //Automatic Brake system 261 | //Because of the responsiveness needed for this brake system to be realistic, every one rolling stock cycle will go through the entire train's brake system 262 | for (var car = 0; car < train.all.length; car++) { 263 | brake.cycle(car); 264 | } 265 | //find the brake force for the one car we're dealing with here 266 | var brakeForce = train.all[i].prototype.brake.brakingForce * sim.direction; 267 | 268 | //Rolling Resistance 269 | train.all[i].prototype.realtime.rollingResistance = sim.direction * -1 * train.all[i].prototype.coeff.rollingResistance * train.all[i].prototype.weight 270 | //This IF statement makes sure we dont accidentally have it pull the train backwards if it's sitting still. 271 | if (train.total.accel.speed.mph == 0) { 272 | train.all[i].prototype.realtime.rollingResistance = 0 273 | } 274 | 275 | //Net Force 276 | var netForce = brakeForce + train.all[i].prototype.realtime.rollingResistance; 277 | if (train.total.accel.speed.mph == 0) { 278 | netForce = 0; 279 | } 280 | train.all[i].prototype.realtime.netForce = netForce; 281 | 282 | //add net force to total 283 | train.total.netForce = train.total.netForce + train.all[i].prototype.realtime.netForce; 284 | 285 | //also ensure we're factoring in this car's weight 286 | train.total.weight = train.total.weight + train.all[i].prototype.weight; //weight = weight + element.weight 287 | } 288 | } 289 | //THE FOR LOOP ENDS HERE 290 | //now we total up all the math we did during the for loop 291 | //convert mass from pounds to kg 292 | train.total.accel.si.mass = train.total.weight * 0.453592; 293 | //convert netForce from pounds to Newtons 294 | train.total.accel.si.force = train.total.netForce * 4.44822; 295 | 296 | //Final Net force/speed calculations 297 | //Defining shorthand variables for clarity 298 | var netForce = train.total.accel.si.force; 299 | var mass = train.total.accel.si.mass; 300 | 301 | //leveraging f=ma to find acceleration, in meters per second per second 302 | train.total.accel.si.acceleration = netForce / mass; 303 | //first we compute the new speed in meters per second 304 | train.total.accel.speed.ms = train.total.accel.speed.ms + train.total.accel.si.acceleration; 305 | 306 | //we store the acceleration in mph per second 307 | train.total.accel.acceleration_mph = train.total.accel.si.acceleration * 2.23694; //convert from m/s/s to mph/s 308 | 309 | //now figure out how much speed to add/subtract by converting that acceleration to miles per hour per sim.time 310 | var accelerationPerCycle = train.total.accel.acceleration_mph * (sim.time.interval / 1000); 311 | //Forced zero crossing code (keeps the train from going back and forth when it should just stop) 312 | if (train.total.accel.speed.mph > 0 && train.total.accel.speed.mph + accelerationPerCycle < 0) { 313 | //if we're going from positive to negative 314 | train.total.accel.speed.mph = 0; 315 | console.info('ZERO CROSSING! (from positive side)') 316 | sim.direction = 0; 317 | } else if (train.total.accel.speed.mph < 0 && train.total.accel.speed.mph + accelerationPerCycle > 0) { 318 | //if we're going from negative to positive 319 | train.total.accel.speed.mph = 0; 320 | console.info('ZERO CROSSING! (from negative side)') 321 | sim.direction = 0; 322 | } else { //if we're not going to cross 0, just handle acceleration like normal 323 | train.total.accel.speed.mph = train.total.accel.speed.mph + accelerationPerCycle; 324 | } 325 | gauge.speedometer(Math.abs(train.total.accel.speed.mph)); //abs in case we're going backwards and it's negative 326 | //also set the sim.direction (actual direction) variable 327 | if (train.total.accel.speed.mph == 0) { 328 | sim.direction = 0; 329 | } else if (train.total.accel.speed.mph > 0) { 330 | sim.direction = 1; 331 | } else if (train.total.accel.speed.mph < 0) { 332 | sim.direction = -1; 333 | } 334 | 335 | //AIR GAUGES 336 | gauge.air.reservoir.equalizing(brake.eqReservoirPSI); 337 | gauge.air.brake.pipe(Math.round(brake.avgLinePSI())); 338 | gauge.air.brake.cylinder(train.all[cab.current].prototype.brake.cylinderPSI); 339 | 340 | //Finally we actually make the locomotive(s) go this speed 341 | for (var x = 0; x < train.all.length; x++) { 342 | //walk over each train element and ignore rolling stock 343 | if (train.all[x].type == "locomotive") { 344 | //set direction first 345 | train.all[x].throttle.direction.set(sim.direction); 346 | train.all[x].dcc.speed.setMPH(Math.abs(train.total.accel.speed.mph), x); //we use ABS here because the direction is set separately from the actual speed. and we pass the train position because reasons 347 | } 348 | } 349 | } 350 | } 351 | 352 | 353 | //SIM INTERVAL STUFF 354 | sim.stop = function() { //stops the sim by clearing the interval 355 | clearInterval(sim.recalcInterval); 356 | } 357 | 358 | sim.start = function(timing) { 359 | if (timing == undefined) { 360 | timing = sim.time.interval; //if the user doesn't specify, we use the last one we used 361 | } 362 | sim.recalcInterval = setInterval(function() { 363 | sim.accel() 364 | }, timing); 365 | sim.time.interval = timing; //store this for later 366 | } 367 | 368 | sim.start(100); //runs the sim every 100ms by default -------------------------------------------------------------------------------- /scripts/stats.js: -------------------------------------------------------------------------------- 1 | stats = { 2 | data : { 3 | 4 | }, 5 | updateData: function() { 6 | stats.data = { 7 | "username" : cfg.anomyousDataUsername, 8 | "browser" : { 9 | productSub: navigator.productSub, 10 | vendor: navigator.vendor, 11 | platform: navigator.platform, 12 | userAgent: navigator.userAgent, 13 | }, 14 | "cfg" : cfg, 15 | "roster" : jmri.roster.entries, 16 | } 17 | }, 18 | send : function() { 19 | log.stats("Sending usage data (you can disable this in /cfg/settings.json)") 20 | stats.updateData(); 21 | var xhr = new XMLHttpRequest(); 22 | xhr.open("POST", "http://zephyrcab-stats.evilgeniustech.com:1189", true); 23 | /* 24 | fun fact, for anyone who is far enough into the code that they're reading this: 25 | there are two reasons I used TCP 1189 as the port for the stats server: 26 | (a) I needed an arbitary port that was sufficiently obscure so as to not interfere with other things 27 | (b) The idea for ZephyrCab originally came to me in the cab of Wabash F7-A #1189 28 | 29 | For anyone concerned about privacy, I do NOT give this data to any third parties. I'm not using it for ad revenue or anything. I'm using it to figure out what kind of user base I have so I can tailor the program to that. 30 | 31 | For the technically minded, the server end of this little stunt is just a simple NodeJS app that pretties up the JSON, timestamps it, adds a public IP address (purely for rough geolocation) and writes that to a JSON log file. 32 | */ 33 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); 34 | xhr.send(JSON.stringify(stats.data)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/train.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | /* 19 | TRAIN 20 | 21 | This file contains all the functions for manipulating and constructing the train. This includes the train builder UI and its backbone! Also defined at the top of this file are the base train objects themselves; they are defined here for organizational purposes. 22 | */ 23 | 24 | //functions dealing with the train go in here, this is for organization 25 | train = new Object(); //first we must define these objects 26 | train.all = []; //THIS IS THE MAIN TRAIN LIST 27 | train.build = new Object(); 28 | train.ui = { 29 | locomotives : {}, 30 | rollingstock : {}, 31 | } 32 | 33 | /* 34 | This function adds the roster information to the bundles.locomotives file entries, and builds the train builder selection UI system thing based on all that information. 35 | */ 36 | train.ui.setup = function() { 37 | //First we go through the keys of the bundles.locomotives to build a list of available locomotive bundles.locomotives. 38 | var locomotivesList = Object.keys(bundles.locomotives); //create an array of strings with the locomotive names 39 | //check for errors to prevent frustration due to naming typos 40 | 41 | /* 42 | This loop goes through each element of the list and finds the corresponding roster entry from the JMRI roster. 43 | Then it adds that entire roster object to bundles.locomotives.thatThing.roster 44 | */ 45 | for(i=0; i < locomotivesList.length; i++) { //now populate .roster subobjects for objects with the names from the array of strings above 46 | bundles.locomotives[locomotivesList[i]].roster = jmri.roster.entries[locomotivesList[i]]; 47 | } 48 | 49 | //Real quick we need to add the rolling stock names to a list 50 | train.ui.rollingstock.names = Object.keys(bundles.rollingstock) 51 | 52 | /* 53 | At this point we can confidently use the bundles.locomotives object to generate anything to do with locomotive availability. It now has all the JMRI roster stuff, so we can get the decoder info from it. 54 | 55 | Now we need to start building the HTML for the train builder. We will store this in an array because it is easier to add to those than a string. At the end, we'll use .join() to combine all of it into a single string and publish it to the DOM. 56 | */ 57 | //update this variable since bundles is dynamic now 58 | train.ui.locomotives.unused = Object.keys(bundles.locomotives); 59 | train.ui.update() 60 | 61 | 62 | } 63 | 64 | /* 65 | These new objects here are for making sure you can't add a locomotive twice. One contains used locomotive roster names, one contains unused ones. This will make it impossible to add a locomotive twice, which would cause the universe to implode. 66 | */ 67 | train.ui.locomotives = new Object(); 68 | train.ui.locomotives.used = []; 69 | train.ui.locomotives.unused = Object.keys(bundles.locomotives); //this actually has to be updated later on 70 | 71 | /* 72 | This function updates the entire train builder area. It should be called whenever a part of the train is edited, or whenever bundles.locomotives.json is edited. 73 | 74 | It is called with no arguments. 75 | */ 76 | train.ui.update = function() { 77 | /* 78 | The first thing we need to tackle is displaying the actual train. 79 | 80 | The train display (the current thing, not the available options) is contained inside document.getElementById("trainDisplay").innerHTML. 81 | It is handled using MaterializeCSS's "chip" feature. 82 | */ 83 | var finalHTML = [] //This variable is going to be combined using join() later on. 84 | for (i=0; i < train.all.length; i++) { 85 | /* 86 | This loop cycles through every single element in the train and generates HTML for each one. 87 | 88 | Right now, the chip element only displays the name of the locomotive and a close button. 89 | 90 | TODO: Once a standardized place to find images is agreed on, I'd like to make use of the great-looking "img" option of these chips. 91 | */ 92 | 93 | var newHTML = []; 94 | 95 | var newHTMLstring = "
"; 96 | newHTML.push(newHTMLstring); 97 | 98 | //LOCOMOTIVES ONLY: This builds the "enter cab" link. 99 | if (train.all[i].type == "locomotive") { 100 | log.trainbuilder("MARKER FOR TYPE==LOCOMOTIVE") 101 | var newHTMLstring = "train" 102 | newHTML.push(newHTMLstring) 103 | var newHTMLstring = ""; 216 | finalHTML.push(newHTML) 217 | } 218 | log.trainbuilder("FINAL HTML: " + finalHTML.join('')) 219 | $("#rollingstockPalette").html(finalHTML.join('')) 220 | 221 | 222 | /* 223 | New Feature: Display locomotive name in CAB tab 224 | 225 | This feature looks at the roster entry name of the lead locomotive and displays it in the CAB tab's spot for names. 226 | 227 | The IF statement is so that if there IS no lead locomotive, we can set the name to "Not Set" 228 | */ 229 | if (train.all[0] != undefined) { 230 | var locoName = train.all[cab.current].roster.name; 231 | } 232 | else { 233 | var locoName = "No Lead Locomotive Found"; 234 | } 235 | /* 236 | ui.cab.locoName.update(locoName) 237 | */ 238 | } 239 | 240 | /* 241 | This function is for adding a bundle object to the train. It doesn't matter if the bundle object is one straight from the bundle files, or if it is one from somewhere else. The beauty of this approach is that this function doesn't care if you're adding locomotives, rolling stock, or some crazy insane new type of thing you made up while half-asleep the other day. 242 | */ 243 | train.build.add = function(objectSource) { 244 | //CLONE THE OBJECT TO AVOID REFERENCING ISSUES - See issue #13 on GitHub for more information on this code 245 | var object = $.extend(true, {}, objectSource) //the first argument here is true to enable recursive "deep copy". Without that, it's useless and doesn't solve the referencing problem 246 | if (object.type == "locomotive") { //This if statement checks if the input object is a locomotive or something else 247 | /* 248 | First we need to define the decoder model and family straight from the roster object. We only do this for convenience. 249 | 250 | After we have this information, we begin actually constructing the new object. 251 | */ 252 | var decoderModel = object.roster.decoderModel; 253 | var decoderFamily = object.roster.decoderFamily; 254 | 255 | var address = object.roster.address; //We need this because the DCC decoder constructor and the throttle need this 256 | 257 | var trainPosition = train.all.length; //this is necessary because the DCC decoder constructor accepts a trainPosition argument 258 | train.all.push(object) 259 | /* 260 | Because of the magical things built into the decoder constructor function spec, we don't need to call a separate create throttle thing. The throttle subobject is automatically created when we add the DCC decoder thing. 261 | 262 | This series of IF/ELSE statements is the mechanism for generic decoder fallback. If ZephyrCab doesn't have a specific object file for a given decoderModel/decoderFamily, it falls back to generic/generic. Otherwise, it just sets the variable decoderConstructor to the appropriate constructor object. 263 | */ 264 | if (decoders[decoderFamily][decoderModel] == undefined) { 265 | decoderConstructor = decoders["generic"]["generic"] 266 | } 267 | else { 268 | var decoderConstructor = decoders[decoderFamily][decoderModel] 269 | } 270 | 271 | train.all[trainPosition].dcc = new decoderConstructor(address, trainPosition) //decoderConstructor here is just shorthand for the actual constructor. It's defined in the IF/ELSE statement above. 272 | 273 | //Now that the entire new object is done, we need to move the locomotive name to the used list 274 | train.ui.locomotives.used.push(object.roster.name) 275 | 276 | //Removing the locomotive from the unused list: 277 | var index = train.ui.locomotives.unused.indexOf(object.roster.name) 278 | train.ui.locomotives.unused.splice(index, 1) 279 | } 280 | //If the input object is rolling stock, do this: 281 | else if (object.type == "rollingstock") { 282 | train.all.push(object) //since there's no decoder or executable object for rolling stock, we literally just add the object as-is 283 | } 284 | 285 | //Now we need to update the train ui 286 | train.ui.update(); 287 | 288 | //We also need to make sure the brakes on this thing are set up right, in case the user has set the feed valve to something other than 90 289 | brake.fixNewElement((train.all.length - 1)) //call this function on the last car 290 | 291 | //DEPRECATED - Left behind for temporary reference 292 | //We only call the gauges.createAll() function when we ADD something, because if there's nothing on the train it'll break things. 293 | //gauge.createAll(); 294 | } 295 | 296 | train.build.remove = function(entryName) { 297 | //figure out which number in the list we're dealing with, thanks to my magical searching function 298 | var index = train.find.all(entryName) 299 | var type = train.all[index].type //this is important info for the rest of the script 300 | 301 | //tell JMRI we're releasing this throttle if it's a locomotive 302 | if (type == "locomotive") { 303 | train.all[index].throttle.release(); //note that you'll get a message from this if debugToasts are enabled. 304 | } 305 | train.all.splice(index, 1) //remove 1 element at the index, basically saying remove the index 306 | debugToast("Removing " + entryName + " from train."); 307 | 308 | //If the object to be removed is a locomotive, we need to update the list of used/unused locomotives. Otherwise, we're basically done. 309 | if (type == "locomotive") { 310 | //Now we have to update the used/unused locomotive lists 311 | var index = train.ui.locomotives.used.indexOf(entryName); 312 | train.ui.locomotives.used.splice(index, 1); 313 | //Now we've removed it from the used list, so we need to add it to the unused list. 314 | train.ui.locomotives.unused.push(entryName) 315 | } 316 | 317 | train.ui.update(); 318 | } 319 | 320 | 321 | /* 322 | This is basically a jerry-rigged version of .indexOf(), but it works with the weird objects-inside-array format of the train.all array. It accepts a name argument and will return the position of the object with that roster.name attribute. 323 | */ 324 | train.find = new Object(); 325 | train.find.all = function(entryName) { 326 | var position; //Go ahead and define this so it's in the right scope 327 | 328 | for (i = 0; i < train.all.length; i++ ) { 329 | var name = train.all[i].roster.name 330 | if (name == entryName) { 331 | position = i; 332 | break; 333 | } 334 | } 335 | return position; 336 | } -------------------------------------------------------------------------------- /scripts/websockets.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | //JSlint Crap 19 | /*global 20 | foo, WebSocket, $, Materialize, console, cfg, train, jmri, ui, air, sim 21 | */ 22 | /*jslint browser:true, white:true, plusplus:true*/ 23 | var train; //declare this just to solve some scoping issues in the Setup app 24 | var link = { 25 | status: false, 26 | latestMessage: "", 27 | logTimestamp: function(type) { 28 | 'use strict'; 29 | if (type === undefined) { 30 | type = 24; 31 | } 32 | 33 | //define variables 34 | var currentDate = new Date(), 35 | hours = currentDate.getHours(), //we'll change this if the function is using 12 hour time 36 | minutes = currentDate.getMinutes(), 37 | seconds = currentDate.getSeconds(), 38 | ampm = "", //if using 12 hour time, we'll set this to "AM" or "PM" 39 | finalString = ""; //what's going to get returned 40 | 41 | if (type === 24) { 42 | finalString = hours + ":" + minutes + ":" + seconds; 43 | } else if (type === 12) { 44 | currentDate = new Date(); 45 | hours = currentDate.getHours(); 46 | if (hours === 24) { 47 | hours = 12; 48 | ampm = "AM"; 49 | } 50 | if (hours > 12) { 51 | hours = hours - 12; 52 | ampm = "PM"; 53 | } 54 | if (hours === 12) { 55 | hours = 12; 56 | ampm = "PM"; 57 | } 58 | finalString = hours + ":" + minutes + ":" + seconds + " " + ampm; 59 | } 60 | return finalString; 61 | }, 62 | 63 | //Connects to WebSockets server and creates the link.socket object 64 | connect: function(ip, port, automaticornot) { 65 | //jslint crap 66 | 'use strict'; 67 | 68 | //check if the function is being called during an autoconnect 69 | if (automaticornot !== true) { 70 | //This is not being called automatically, so set the parameter to false 71 | automaticornot = false; 72 | } 73 | 74 | //Initialize a new WebSocket...all JMRI installs will use /json as the path 75 | link.socket = new WebSocket("ws://" + ip + ":" + port + "/json/"); 76 | 77 | /* 78 | On connection open... 79 | - Indicate newly opened connection status 80 | - Start the heartbeats to keep it alive 81 | - Create listeners so JMRI will continually update us with new info on relevant things 82 | - Display a Materialize.toast about the connection 83 | */ 84 | link.socket.onopen = function() { 85 | link.status = true; 86 | //indicate the connection in the UI 87 | $("#connectionStatus").html("Connected!").css("color", "green"); 88 | 89 | //enter it in the log 90 | log.websockets("Connection opened with " + cfg.ip + ":" + cfg.port); 91 | 92 | //TODO - fix global 93 | //start the heartbeats to keep it alive 94 | var heartbeatInterval = setInterval( 95 | //Heartbeats function embedded into this setInterval call 96 | function() { 97 | link.send('{"type":"ping"}'); 98 | }, 99 | 6000 100 | ); 101 | log.websockets("Beginning heartbeats..."); 102 | 103 | /* 104 | LISTENERS - Send a blank command as a request, and JMRI will continually update us on the status of whatever thing... 105 | - Read more on this at JMRI WebSockets documentation 106 | */ 107 | link.send('{"type":"power","data":{}}'); 108 | link.send('{"list":"roster"}'); 109 | 110 | //Display the appropriate connection message 111 | if (automaticornot === true) { 112 | //If this connection attempt was automatic 113 | Materialize.toast("Connected automatically to ws://" + cfg.ip + ":" + cfg.port, 3000); 114 | } else { 115 | //If this connection attempt was not automatic 116 | Materialize.toast("Connected manually to ws://" + cfg.ip + ":" + cfg.port, 3000); 117 | } 118 | 119 | setTimeout(function(){setup.onConnect();}, 500); //wait 500ms to give JMRI time to send info before we load the setup page info 120 | }; 121 | 122 | //Upon receiving a message, log it if logging is enabled, then send it off to the handler 123 | link.socket.onmessage = function(event) { 124 | var data = JSON.parse(event.data), 125 | stringified; 126 | if (cfg.logallmessages === true) { 127 | stringified = JSON.stringify(data); 128 | log.websockets("[" + link.logTimestamp() + "] RECEIVED : " + stringified); 129 | } 130 | link.process(data); 131 | }; 132 | 133 | //Simple function to throw an error if the WebSocket closes 134 | link.socket.onclose = function() { 135 | console.error("WebSocket Closed..."); 136 | Materialize.toast("warningLost connection to JMRI! Please refresh the page and reconnect. Reload"); 137 | }; 138 | }, 139 | 140 | //Simple wrapper function providing errors/logging for sending WebSockets commands 141 | send: function(command) { 142 | 'use strict'; 143 | //If we're connected, send the command 144 | if (link.status === true) { 145 | link.socket.send(command); 146 | //If logging is enabled, make a console.log entry 147 | if (cfg.logallmessages === true) { 148 | log.websockets("[" + link.logTimestamp() + "] SENT : " + command); 149 | } 150 | 151 | //If we're not connected, throw an error 152 | } else { 153 | Materialize.toast("errorYou need to connect to JMRI first!", 4000); 154 | console.error("Cannot send commands - no JMRI connection!"); 155 | } 156 | }, 157 | 158 | /* 159 | AUTOCONNECT 160 | 161 | This function is what makes the auto connect feature work. It is at the bottom of index.html, so it runs when the page loads. 162 | It looks at the cfg object to see if the variables we need are configured or not, and connects if they are set up. 163 | */ 164 | autoconnect: function() { 165 | 'use strict'; 166 | //if we know it's running on an external webserver based on the cfg settings 167 | if (cfg.webServer === "external") { 168 | //If it's configured, try to connect 169 | if (cfg.ip !== undefined) { 170 | link.connect(cfg.ip, cfg.port, true); //the third argument tells the connect() that it's being called automatically 171 | $("#link-ip").val(cfg.ip); 172 | $("#link-port").val(cfg.port); 173 | 174 | //These two lines go over to the trainsettings tab and scroll up to the top of the page. 175 | $("#tab_trainsettings").click(); 176 | document.body.scrollTop = document.documentElement.scrollTop = 0; 177 | 178 | //If not, tell the user to connect manually 179 | } else if (cfg.ip === undefined) { 180 | log.websockets("No autoconnection settings found!"); 181 | Materialize.toast("infoCouldn't find any settings for auto-connection, please connect manually."); 182 | 183 | //These two lines go over to the trainsettings tab and scroll up to the top of the page. 184 | $("#tab_trainsettings").click(); 185 | document.body.scrollTop = document.documentElement.scrollTop = 0; 186 | } 187 | } 188 | //if we're assuming it's running on a JMRI Jetty web server 189 | else { 190 | //set IP:Port to local JMRI instance 191 | cfg.ip = location.hostname; 192 | if (location.port === "") { //if there is no port, recognize that it's actually port 80 193 | cfg.port = 80; 194 | } 195 | else { //otherwise, use the port provided by the browser 196 | cfg.port = Number(location.port); 197 | } 198 | link.connect(cfg.ip, cfg.port, true); //the third argument tells the connect() that it's being called automatically 199 | $("#link-ip").val(cfg.ip); 200 | $("#link-port").val(cfg.port); 201 | 202 | //These two lines go over to the trainsettings tab and scroll up to the top of the page. 203 | $("#tab_trainsettings").click(); 204 | document.body.scrollTop = document.documentElement.scrollTop = 0; 205 | } 206 | }, 207 | 208 | /* 209 | MESSAGE HANDLER 210 | 211 | This function is called whenever the WebSocket instance (at link.socket) receives a message. It handles passing said message data on to the appropriate recipient based on its "type" field. 212 | */ 213 | process: function(ev) { 214 | 'use strict'; 215 | link.latestMessage = ev; 216 | 217 | if (ev.type === "pong") { 218 | //just ignore these, I only have this IF in case I want to log these in a special way or something. 219 | return; //placeholder to make jslint shut up 220 | } else if (ev.type === "hello") { 221 | //sets up initial info about railroad 222 | jmri.hellomsg = ev; 223 | jmri.railroadName = ev.data.railroad; 224 | } else if (ev.type === "throttle") { 225 | //send to throttle info handler 226 | //TODO, if we even need this with how complex this is and how un-normal it is compared to traditional DCC 227 | return; 228 | } else if (ev.type === "sensor") { 229 | //send to sensor info handler 230 | return; 231 | } else if (ev.type === "turnout") { 232 | //send to turnout info handler 233 | return; //placeholder to make jslint shut up 234 | } else if (ev.type === "power") { 235 | //send to layout power info handler 236 | jmri.handleType.power(ev); 237 | } else if (ev.list) { 238 | //send to list handler 239 | return; //placeholder to make jslint shut up 240 | } else if (ev[0] !== undefined) { 241 | if (ev[0].type === "rosterEntry") { 242 | jmri.roster.raw = ev; 243 | jmri.roster.entries = jmri.roster.reformat(jmri.roster.raw); //rebuild the reformatted roster every time we get new roster data 244 | 245 | //Now that the roster has been edited, we need to update the UI for the train, but only if we're in the main app not the setup page 246 | if (train != undefined) { 247 | train.ui.setup(); 248 | train.ui.update(); 249 | } 250 | } 251 | } 252 | } 253 | }; -------------------------------------------------------------------------------- /soundfx/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/soundfx/click.mp3 -------------------------------------------------------------------------------- /soundfx/switch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/soundfx/switch.mp3 -------------------------------------------------------------------------------- /standards/decoders.standards.js: -------------------------------------------------------------------------------- 1 | /* 2 | ZephyrCab - Realistic Model Train Simulation/Control System 3 | Copyright (C) 2017 Hampton Morgan (K4KFH) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published 7 | by the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | */ 18 | //Constructors will be called with address, trainPosition arguments and should produce an object structured like this: 19 | 20 | generic = { 21 | f: { 22 | headlight: { 23 | set: function(state) {}, 24 | state: false, 25 | }, 26 | bell: { 27 | set: function(state) {}, 28 | state: false, 29 | }, 30 | horn: { 31 | set: function(state) {}, 32 | state: false, 33 | }, 34 | compressor: { 35 | set: function(state) {}, 36 | state: false, 37 | }, 38 | airDump: { 39 | set: function(state) {}, 40 | state: false, 41 | }, 42 | dynBrakes: { 43 | set: function(state) {}, 44 | state: false, 45 | }, 46 | engine: { 47 | set: function(state) { 48 | //note that this function MUST set the following variable! 49 | if (state === true) { 50 | train.all[trainPosition].prototype.engineRunning = 1; 51 | } else if (state === false) { 52 | train.all[trainPosition].prototype.engineRunning = 0; 53 | } 54 | }, 55 | state : false, 56 | }, 57 | notch: { 58 | up: function(){ 59 | //include any necessary error checking and such for your decoder, and be aware that use of setTimeout() may be necessary 60 | //Also note that you MUST increment this variable! 61 | var newNotch = (train.all[trainPosition].dcc.f.notch.state + 1); 62 | }, 63 | down: function(){ 64 | //include any necessary error checking and such for your decoder, and be aware that use of setTimeout() may be necessary 65 | //Also note that you MUST decrement this variable! 66 | var newNotch = (train.all[trainPosition].dcc.f.notch.state - 1); 67 | }, 68 | state: 0, //This should reflect the current notching state of the sound decoder. You should increment this up or down 1 when your up() and down() functions finish, or sim.js's functions will be horribly confused and mess up your sounds. 69 | }, 70 | }, 71 | /* 72 | The speed code is NOT decoder specific. Please copy and paste this code: 73 | //SPEED SETTING 74 | this.speed = {}; 75 | this.speed.state = 0; 76 | this.speed.set = function (speed) { 77 | train.all[trainPosition].throttle.speed.set(speed); 78 | train.all[trainPosition].dcc.speed.state = speed; 79 | }; 80 | this.speed.setMPH = function (mph) { 81 | var speed = train.all[trainPosition].model.speed(mph); 82 | train.all[trainPosition].dcc.speed.set(speed); 83 | }; 84 | */ 85 | speed: { //DO NOT write this yourself. Copy and paste what's above!! 86 | state: 0, 87 | set: function(speed){}, 88 | setMPH: function(mph){}, 89 | } 90 | } -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/README: -------------------------------------------------------------------------------- 1 | Generic info 2 | ================= 3 | 4 | This is tiny implementation of gauge using pure JavaScript and HTML5 canvas 5 | No need to use any library. 6 | 7 | Why canvas gauge? 8 | ================= 9 | 10 | Because it is compatible with most modern browsers and with mobile devices. 11 | For example, SVG, does not work on Android 2.x by default, but canvas do work. 12 | BTW you can find your own reasons why to use canvas. 13 | 14 | Documentation 15 | ================= 16 | 17 | Please, read wiki for more information at https://github.com/Mikhus/canv-gauge/wiki. 18 | 19 | Examples 20 | ================= 21 | 22 | http://ru.smart-ip.net/gauge.html 23 | http://ru.smart-ip.net/gauge1.html 24 | http://ru.smart-ip.net/gauge2.html 25 | http://ru.smart-ip.net/gauge-html.html 26 | Real usage example: http://ru.smart-ip.net/speed-test 27 | 28 | License 29 | ================= 30 | This code is subject to MIT license. 31 | 32 | Copyright (c) 2012 Mykhailo Stadnyk 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy of 35 | this software and associated documentation files (the "Software"), to deal in 36 | the Software without restriction, including without limitation the rights to use, 37 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 38 | Software, and to permit persons to whom the Software is furnished to do so, 39 | subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all 42 | copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 46 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 47 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 48 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 49 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/build.bat: -------------------------------------------------------------------------------- 1 | java -jar compiler.jar --warning_level VERBOSE --compilation_level SIMPLE_OPTIMIZATIONS --summary_detail_level 3 --create_source_map ./gauge.min.js.map --source_map_format=V3 --js gauge.js --js_output_file gauge.min.js 2 | echo done -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | java -jar compiler.jar --warning_level VERBOSE --compilation_level SIMPLE_OPTIMIZATIONS --summary_detail_level 3 --create_source_map ./gauge.min.js.map --source_map_format=V3 --js gauge.js --js_output_file gauge.min.js -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/thirdparty/canv-gauge-master/compiler.jar -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/example-html-gauge.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gtest 5 | 6 | 7 | 8 | 9 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/example-resize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gauge Test 6 | 7 | 8 | 9 | 10 | 11 |
12 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gauge Test 6 | 7 | 8 | 9 | 10 |

11 | 12 |
13 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/fonts/digital-7-mono.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/thirdparty/canv-gauge-master/fonts/digital-7-mono.eot -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/fonts/digital-7-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k4kfh/ZephyrCab/9363b26cc55efeddee404729b75a4017a2bdb940/thirdparty/canv-gauge-master/fonts/digital-7-mono.ttf -------------------------------------------------------------------------------- /thirdparty/canv-gauge-master/gauge.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | HTML5 Canvas Gauge implementation 4 | 5 | This code is subject to MIT license. 6 | 7 | Copyright (c) 2012 Mykhailo Stadnyk 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of 10 | this software and associated documentation files (the "Software"), to deal in 11 | the Software without restriction, including without limitation the rights to use, 12 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 13 | Software, and to permit persons to whom the Software is furnished to do so, 14 | subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 21 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 22 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 23 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | @authors: Mykhailo Stadnyk 27 | Chris Poile 28 | Luca Invernizzi 29 | Robert Blackburn 30 | */ 31 | var Gauge=function(b){function l(a,b){for(var c in b)"object"==typeof b[c]&&"[object Array]"!==Object.prototype.toString.call(b[c])&&"renderTo"!=c?("object"!=typeof a[c]&&(a[c]={}),l(a[c],b[c])):a[c]=b[c]}function q(){z.width=b.width;z.height=b.height;A=z.cloneNode(!0);B=A.getContext("2d");C=z.width;D=z.height;t=C/2;u=D/2;f=tc;c++)b.majorTicks.push(w(b.minValue+h*c));b.majorTicks.push(w(b.maxValue))}for(c=0;ca;a=Math.abs(a);if(0n?Math.abs(b.minValue-n):0b.maxValue?b.maxValue+d:a=(7-4*b)/11){a=-Math.pow((11-6*b-11*a)/4,2)+Math.pow(c,2);break a}a=void 0}return 1-a},elastic:function(a){a=1-a;return 1-Math.pow(2,10*(a-1))*Math.cos(30*Math.PI/3*a)}},G=null;a.lineCap="round";this.draw=function(){if(!A.i8d){B.clearRect(-t,-u,C,D);B.save();var g={ctx:a};a=B;p();N();J();d();s();b.title&&(a.save(),a.font=24*(f/200)+"px Arial",a.fillStyle=b.colors.title,a.textAlign="center",a.fillText(b.title,0,-f/4.25),a.restore());b.units&&(a.save(),a.font=22*(f/200)+"px Arial", 46 | a.fillStyle=b.colors.units,a.textAlign="center",a.fillText(b.units,0,f/3.25),a.restore());A.i8d=!0;a=g.ctx;delete g.ctx}a.clearRect(-t,-u,C,D);a.save();a.drawImage(A,-t,-u,C,D);if(Gauge.initialized)L(),K(),H||(E.onready&&E.onready(),H=!0);else var e=setInterval(function(){Gauge.initialized&&(clearInterval(e),L(),K(),H||(E.onready&&E.onready(),H=!0))},10);return this}};Gauge.initialized=!1; 47 | (function(){var b=document,l=b.getElementsByTagName("head")[0],q=-1!=navigator.userAgent.toLocaleLowerCase().indexOf("msie"),v="@font-face {font-family: 'Led';src: url('fonts/digital-7-mono."+(q?"eot":"ttf")+"');}",k=b.createElement("style");k.type="text/css";if(q)l.appendChild(k),l=k.styleSheet,l.cssText=v;else{try{k.appendChild(b.createTextNode(v))}catch(e){k.cssText=v}l.appendChild(k);l=k.styleSheet?k.styleSheet:k.sheet||b.styleSheets[b.styleSheets.length-1]}var g=setInterval(function(){if(b.body){clearInterval(g); 48 | var e=b.createElement("div");e.style.fontFamily="Led";e.style.position="absolute";e.style.height=e.style.width=0;e.style.overflow="hidden";e.innerHTML=".";b.body.appendChild(e);setTimeout(function(){Gauge.initialized=!0;e.parentNode.removeChild(e)},250)}},1)})();Gauge.Collection=[]; 49 | Gauge.Collection.get=function(b){if("string"==typeof b)for(var l=0,q=this.length;l