400 lines
14 KiB
C++
400 lines
14 KiB
C++
/* PacoMouseCYD throttle -- F. Cañada 2025-2026 -- https://usuaris.tinet.cat/fmco/
|
|
*/
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// ***** STEAM THROTTLE *****
|
|
////////////////////////////////////////////////////////////
|
|
|
|
void initSteamThrottle () {
|
|
uint16_t n;
|
|
if (oldSteamLoco != locoData[myLocoData].myAddr.address) {
|
|
oldSteamLoco = locoData[myLocoData].myAddr.address;
|
|
steamPressure = 80;
|
|
waterLevelBoiler = 80;
|
|
waterLevelTender = 350;
|
|
oldPressure = 0;
|
|
oldLevelBoiler = 0;
|
|
oldLevelTender = 0;
|
|
barData[BAR_JOHNSON].value = STEAM_JOHNSON_NEUTRAL; // neutral position
|
|
barData[BAR_BRAKE].value = 0;
|
|
steamDir = locoData[myLocoData].myDir;
|
|
for (n = 0; n < MAX_LIMIT; n++)
|
|
steamSpeedLimit[n] = LIMIT_NONE;
|
|
}
|
|
currentSteamTime = millis();
|
|
steamTimeSpeed = currentSteamTime;
|
|
steamTimeSteam = currentSteamTime;
|
|
steamTimeWater = currentSteamTime;
|
|
steamTimeLoad = currentSteamTime;
|
|
steamTimeSmoke = currentSteamTime;
|
|
shovelCoal = false;
|
|
setFirebox();
|
|
endWaterInjection();
|
|
endTenderFill();
|
|
setTimer(TMR_STEAM, 5, TMR_ONESHOT);
|
|
changeSmoke = STEAM_SMOKE_FAST;
|
|
oldPressure = 0;
|
|
oldSpeedSteam = 305;
|
|
encoderMax = 31; // set throttle to current speed
|
|
updateSteamThrottle();
|
|
if (steamDir != locoData[myLocoData].myDir) { // check Johnson bar position
|
|
steamDir = locoData[myLocoData].myDir;
|
|
barData[BAR_JOHNSON].value = 6 - barData[BAR_JOHNSON].value;
|
|
}
|
|
}
|
|
|
|
void updateSteamThrottle() {
|
|
switch (wifiSetting.protocol) {
|
|
case CLIENT_Z21:
|
|
case CLIENT_XNET:
|
|
if (bitRead(locoData[myLocoData].mySteps, 2)) { // 0..127
|
|
currentSteamSpeed = locoData[myLocoData].mySpeed;
|
|
encoderValue = currentSteamSpeed >> 2;
|
|
}
|
|
else {
|
|
if (bitRead(locoData[myLocoData].mySteps, 1)) { // 0..31
|
|
encoderValue = (locoData[myLocoData].mySpeed & 0x0F) << 1;
|
|
if (bitRead(locoData[myLocoData].mySpeed, 4))
|
|
bitSet(encoderValue, 0);
|
|
currentSteamSpeed = (encoderValue > 3) ? (encoderValue << 2) + (encoderValue >> 3) : 0;
|
|
}
|
|
else { // 0..15
|
|
encoderValue = locoData[myLocoData].mySpeed & 0x0F;
|
|
encoderValue = (encoderValue > 1) ? encoderValue << 1 : 0;
|
|
currentSteamSpeed = (encoderValue << 2) + (encoderValue >> 3);
|
|
}
|
|
}
|
|
break;
|
|
case CLIENT_LNET:
|
|
case CLIENT_ECOS:
|
|
currentSteamSpeed = locoData[myLocoData].mySpeed;
|
|
encoderValue = currentSteamSpeed >> 2;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void steamProcess() {
|
|
uint16_t value, newSpeed, mappedThrottle, jFactor;
|
|
|
|
steamSpeedLimit[LIMIT_THROTTLE] = (encoderValue << 2) + (encoderValue >> 3); // Read Throtle Speed (0..31 -> 0..127)
|
|
|
|
steamSpeedLimit[LIMIT_JOHNSON] = LIMIT_NONE;
|
|
switch (barData[BAR_JOHNSON].value) { // Read Johnson Bar
|
|
case 0: // full reverse
|
|
jFactor = 3;
|
|
changeSpeed = 80;
|
|
steamDir = 0x00;
|
|
break;
|
|
case 1:
|
|
jFactor = 2;
|
|
changeSpeed = 200;
|
|
steamDir = 0x00;
|
|
break;
|
|
case 2:
|
|
jFactor = 1;
|
|
changeSpeed = 500;
|
|
steamDir = 0x00;
|
|
break;
|
|
case STEAM_JOHNSON_NEUTRAL: // Neutral position of Johnson Bar
|
|
jFactor = 1;
|
|
changeSpeed = 1000;
|
|
steamSpeedLimit[LIMIT_JOHNSON] = 0;
|
|
break;
|
|
case 4:
|
|
jFactor = 1;
|
|
changeSpeed = 500;
|
|
steamDir = 0x80;
|
|
break;
|
|
case 5:
|
|
jFactor = 2;
|
|
changeSpeed = 200;
|
|
steamDir = 0x80;
|
|
break;
|
|
case 6: // full forward
|
|
jFactor = 3;
|
|
changeSpeed = 80;
|
|
steamDir = 0x80;
|
|
break;
|
|
}
|
|
|
|
if (steamDir != locoData[myLocoData].myDir) { // Check direction
|
|
locoData[myLocoData].myDir = steamDir;
|
|
changeDirection();
|
|
DEBUG_MSG("STEAM: Change direction")
|
|
}
|
|
|
|
value = steamSpeedLimit[LIMIT_THROTTLE];
|
|
changeSteam = 10000 - ((value * 9) * jFactor); // Steam timeout: 6571 to 10000
|
|
changeWater = 14000 - ((value * 27) * jFactor); // Water timeout: 3713 to 14000
|
|
if (barData[BAR_BRAKE].value > 0) { // Brake bar: 300, 150, 100
|
|
changeSpeed = 300 / barData[BAR_BRAKE].value;
|
|
}
|
|
currentSteamTime = millis();
|
|
|
|
if (currentSteamTime > (steamTimeWater + changeWater)) { // Water consumption
|
|
steamTimeWater = currentSteamTime;
|
|
if (waterLevelBoiler > 0) {
|
|
waterLevelBoiler--;
|
|
DEBUG_MSG("Boiler Level: %d", waterLevelBoiler)
|
|
}
|
|
}
|
|
if (waterLevelBoiler < 10) { // Stop loco if not enough water
|
|
steamSpeedLimit[LIMIT_WATER] = 0;
|
|
steamThrottleStop();
|
|
}
|
|
else {
|
|
steamSpeedLimit[LIMIT_WATER] = LIMIT_NONE;
|
|
}
|
|
|
|
if (currentSteamTime > (steamTimeSteam + changeSteam)) { // Steam consumption
|
|
steamTimeSteam = currentSteamTime;
|
|
if (steamPressure > 0)
|
|
steamPressure--;
|
|
}
|
|
|
|
if (steamPressure < 50) { // Limit speed based on steam level
|
|
value = (steamPressure < 20) ? 0 : map(steamPressure, 20, 50, 20, 120);
|
|
steamSpeedLimit[LIMIT_PRESSURE] = value;
|
|
}
|
|
else {
|
|
steamSpeedLimit[LIMIT_PRESSURE] = LIMIT_NONE;
|
|
}
|
|
|
|
if (currentSteamTime > (steamTimeLoad + STEAM_LOAD_TIME)) { // Load coal and water
|
|
steamTimeLoad = currentSteamTime;
|
|
if (shovelCoal) { // Fire open for shoveling coal
|
|
if (steamPressure > 96) {
|
|
shovelCoal = false;
|
|
setFirebox();
|
|
newEvent(OBJ_FNC, FNC_ST_FIRE, EVNT_DRAW);
|
|
}
|
|
else {
|
|
if (steamPressure < 20) // slowly pressure up at beginning
|
|
steamPressure += 1;
|
|
else
|
|
steamPressure += 2;
|
|
}
|
|
}
|
|
if (waterInjection) { // Water injector open
|
|
if (waterLevelTender > 0) { // Inject water with water from tender
|
|
if (waterLevelBoiler > 95) {
|
|
endWaterInjection();
|
|
}
|
|
else {
|
|
waterLevelBoiler += 2;
|
|
waterLevelTender--;
|
|
}
|
|
steamSpeedLimit[LIMIT_TENDER] = LIMIT_NONE;
|
|
}
|
|
else {
|
|
endWaterInjection(); // Stop locomotive if tender empty
|
|
steamSpeedLimit[LIMIT_TENDER] = 0;
|
|
steamThrottleStop();
|
|
}
|
|
}
|
|
if (fillTender) {
|
|
if ((waterLevelTender > 495) || (currentSteamSpeed != 0)) { // Only fill tender when stopped
|
|
endTenderFill();
|
|
}
|
|
else {
|
|
waterLevelTender++;
|
|
if (waterLevelTender > 6) // Minimum level to run again
|
|
steamSpeedLimit[LIMIT_TENDER] = LIMIT_NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentSteamTime > (steamTimeSmoke + changeSmoke)) { // Chimney smoke
|
|
steamTimeSmoke = currentSteamTime;
|
|
if (currentSteamSpeed > 0) {
|
|
fncData[FNC_ST_SMOKE].state = !fncData[FNC_ST_SMOKE].state;
|
|
changeSmoke = map(currentSteamSpeed, 0, 127, STEAM_SMOKE_SLOW, STEAM_SMOKE_FAST);
|
|
}
|
|
else {
|
|
fncData[FNC_ST_SMOKE].state = false;
|
|
changeSmoke = STEAM_SMOKE_SLOW + STEAM_SMOKE_SLOW;
|
|
}
|
|
newEvent(OBJ_FNC, FNC_ST_SMOKE, EVNT_DRAW);
|
|
}
|
|
|
|
if (barData[BAR_BRAKE].value > 0) { // Braking
|
|
value = barData[BAR_BRAKE].value * 8;
|
|
value = (currentSteamSpeed > value) ? (currentSteamSpeed - value) : 0;
|
|
steamSpeedLimit[LIMIT_BRAKE] = value;
|
|
}
|
|
else {
|
|
steamSpeedLimit[LIMIT_BRAKE] = LIMIT_NONE;
|
|
}
|
|
|
|
targetSpeedSteam = LIMIT_NONE; // Find lower limit
|
|
for (value = 0; value < MAX_LIMIT; value++) {
|
|
if (steamSpeedLimit[value] < targetSpeedSteam)
|
|
targetSpeedSteam = steamSpeedLimit[value];
|
|
}
|
|
|
|
newSpeed = currentSteamSpeed;
|
|
if (currentSteamTime > (steamTimeSpeed + changeSpeed)) { // Speed acceleration
|
|
steamTimeSpeed = currentSteamTime;
|
|
//DEBUG_MSG("Target: %d Current: %d New: %d", targetSpeedSteam, currentSteamSpeed, newSpeed)
|
|
if (targetSpeedSteam > currentSteamSpeed) {
|
|
newSpeed = currentSteamSpeed + 1;
|
|
DEBUG_MSG("Inc New: %d", newSpeed)
|
|
}
|
|
if (targetSpeedSteam < currentSteamSpeed) {
|
|
newSpeed = currentSteamSpeed - 1;
|
|
DEBUG_MSG("Dec New: %d", newSpeed)
|
|
}
|
|
//DEBUG_MSG("New acc: %d", newSpeed)
|
|
}
|
|
|
|
|
|
if (currentSteamSpeed != newSpeed) { // changes in speed
|
|
DEBUG_MSG("Step: %d - New: %d LIMITS ", currentSteamSpeed, newSpeed)
|
|
#ifdef DEBUG
|
|
for (value = 0; value < MAX_LIMIT; value++) {
|
|
Serial.print(steamSpeedLimit[value]);
|
|
Serial.print(" ");
|
|
}
|
|
Serial.println();
|
|
#endif
|
|
currentSteamSpeed = newSpeed;
|
|
switch (wifiSetting.protocol) {
|
|
case CLIENT_Z21:
|
|
case CLIENT_XNET:
|
|
if (bitRead(locoData[myLocoData].mySteps, 2)) { // 128 steps
|
|
mappedThrottle = (currentSteamSpeed > 1) ? currentSteamSpeed : 0;
|
|
}
|
|
else {
|
|
if (bitRead(locoData[myLocoData].mySteps, 1)) { // 28 steps
|
|
if (currentSteamSpeed > 15) {
|
|
mappedThrottle = currentSteamSpeed >> 3;
|
|
bitWrite(mappedThrottle, 4, bitRead(currentSteamSpeed, 2));
|
|
}
|
|
else {
|
|
mappedThrottle = 0;
|
|
}
|
|
}
|
|
else { // 14 steps
|
|
mappedThrottle = (currentSteamSpeed > 15) ? currentSteamSpeed >> 3 : 0;
|
|
}
|
|
}
|
|
break;
|
|
case CLIENT_LNET:
|
|
case CLIENT_ECOS:
|
|
mappedThrottle = (currentSteamSpeed > 1) ? currentSteamSpeed : 0;
|
|
break;
|
|
}
|
|
locoData[myLocoData].mySpeed = mappedThrottle;
|
|
locoOperationSpeed();
|
|
DEBUG_MSG("Steam step: %d", currentSteamSpeed)
|
|
if ((currentSteamSpeed > 0) && (changeSmoke > STEAM_SMOKE_SLOW)) { // initial chuff
|
|
changeSmoke = 0;
|
|
fncData[FNC_ST_SMOKE].state = false;
|
|
}
|
|
}
|
|
|
|
if ((oldLevelTender / 10) != (waterLevelTender / 10)) {
|
|
oldLevelTender = waterLevelTender;
|
|
barData[BAR_TENDER].value = waterLevelTender;
|
|
newEvent(OBJ_BAR, BAR_TENDER, EVNT_DRAW);
|
|
}
|
|
|
|
value = waterLevelBoiler / 2;
|
|
if (oldLevelBoiler != value ) {
|
|
oldLevelBoiler = value;
|
|
barData[BAR_WATER].value = value;
|
|
newEvent(OBJ_BAR, BAR_WATER, EVNT_DRAW);
|
|
}
|
|
|
|
value = steamPressure * 270 / 100;
|
|
if (oldPressure != value) {
|
|
showPressure(value);
|
|
DEBUG_MSG("Pressure: %d", steamPressure)
|
|
}
|
|
}
|
|
|
|
|
|
void startWaterInjection () {
|
|
waterInjection = true;
|
|
fncData[FNC_ST_WATER].colorOn = COLOR_DARKGREEN;
|
|
drawObject(OBJ_FNC, FNC_ST_WATER);
|
|
}
|
|
|
|
|
|
void endWaterInjection () {
|
|
waterInjection = false;
|
|
fncData[FNC_ST_WATER].colorOn = COLOR_RED;
|
|
drawObject(OBJ_FNC, FNC_ST_WATER);
|
|
}
|
|
|
|
|
|
void startTenderFill() {
|
|
fillTender = true;
|
|
//fncData[FNC_ST_TENDER].color = COLOR_RED;
|
|
fncData[FNC_ST_TENDER].colorOn = COLOR_DARKGREEN;
|
|
drawObject(OBJ_FNC, FNC_ST_TENDER);
|
|
}
|
|
|
|
|
|
void endTenderFill() {
|
|
fillTender = false;
|
|
//fncData[FNC_ST_TENDER].color = COLOR_WHITE;
|
|
fncData[FNC_ST_TENDER].colorOn = COLOR_RED;
|
|
drawObject(OBJ_FNC, FNC_ST_TENDER);
|
|
}
|
|
|
|
|
|
void setFirebox() {
|
|
if (shovelCoal) {
|
|
fncData[FNC_ST_FIRE].idIcon = FNC_FIRE_OP_OFF;
|
|
fncData[FNC_ST_FIRE].color = COLOR_ORANGE;
|
|
fncData[FNC_ST_FIRE].colorOn = COLOR_YELLOW;
|
|
}
|
|
else {
|
|
fncData[FNC_ST_FIRE].idIcon = FNC_FIRE_CL_OFF;
|
|
fncData[FNC_ST_FIRE].color = COLOR_SILVER;
|
|
fncData[FNC_ST_FIRE].colorOn = COLOR_RED;
|
|
}
|
|
}
|
|
|
|
|
|
void steamThrottleStop() { // set controls for stop
|
|
if (encoderValue > 0) {
|
|
encoderValue = 0;
|
|
showSpeedSteam(240);
|
|
}
|
|
if (barData[BAR_JOHNSON].value != STEAM_JOHNSON_NEUTRAL) {
|
|
barData[BAR_JOHNSON].value = STEAM_JOHNSON_NEUTRAL; // Neutral
|
|
drawObject(OBJ_BAR, BAR_JOHNSON);
|
|
}
|
|
}
|
|
|
|
|
|
void showPressure(uint16_t angle) {
|
|
tft.setPivot(140, 105); // Set pivot to center of manometer in TFT screen
|
|
sprite.setColorDepth(8); // Create an 8bpp Sprite
|
|
sprite.createSprite(19, 19); // 8bpp requires 19 * 19 = 361 bytes
|
|
sprite.setPivot(9, 9); // Set pivot relative to top left corner of Sprite
|
|
sprite.fillSprite(COLOR_WHITE); // Fill the Sprite with background
|
|
sprite.pushRotated(oldPressure);
|
|
oldPressure = angle;
|
|
sprite.drawBitmap(0, 0, needle_bar, 19, 19, COLOR_BLUE);
|
|
sprite.pushRotated(angle);
|
|
sprite.deleteSprite();
|
|
}
|
|
|
|
void showSpeedSteam(uint16_t angle) {
|
|
tft.setPivot(120, 170); // Set pivot to center of bar in TFT screen
|
|
sprite.setColorDepth(8); // Create an 8bpp Sprite
|
|
sprite.createSprite(83, 15); // 8bpp requires 83 * 15 = 1245 bytes
|
|
sprite.setPivot(76, 7); // Set pivot relative to top left corner of Sprite
|
|
sprite.fillSprite(COLOR_BLACK); // Fill the Sprite with background
|
|
sprite.pushRotated(oldSpeedSteam);
|
|
oldSpeedSteam = angle;
|
|
sprite.drawBitmap(0, 0, speed_steam, 83, 15, COLOR_RED);
|
|
sprite.pushRotated(angle);
|
|
tft.drawArc(120, 170, 27, 22, 315, 45, COLOR_RED, COLOR_BLACK, false);
|
|
sprite.deleteSprite();
|
|
}
|