├── .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 | [](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 | [](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 | [](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 | 
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 | 
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 | 
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 | 
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 = "