|
/** |
|
* Jackrabbit Industries Automation Controller (JRIAC) |
|
* Version 1.1 |
|
*/ |
|
|
|
// CONSTANTS |
|
const string BlockPrefix = "[Automated]"; |
|
//const string GroupPrefix = "[Automated]"; //TODO |
|
|
|
// Set Programmable Block screen to 0.4 font size, recommend Monospace font. |
|
const int displayBufferX = 64; |
|
const int displayBufferY = 27; |
|
|
|
public void DebugPrint (string str) { |
|
if (str.Length > displayBufferX - 1) { |
|
str = str.Substring (0, displayBufferX - 1); |
|
} |
|
displayBuffer.Add (str + "\n"); |
|
if (displayBuffer.Count > displayBufferY) { |
|
displayBuffer.RemoveAt (0); |
|
} |
|
Me.GetSurface (0).WriteText (String.Concat (displayBuffer)); |
|
} |
|
|
|
public enum EventType { |
|
None, |
|
OnClose, |
|
OnClosing, |
|
OnOpen, |
|
OnOpening, |
|
OnDetect, |
|
OnLost |
|
} |
|
string[] EventTypeNames = Enum.GetNames (typeof (EventType)); |
|
|
|
public enum CommandType { |
|
None, |
|
Close, |
|
Open, |
|
Pressurize, |
|
Depressurize, |
|
TurnOn, |
|
TurnOff, |
|
ToggleDoor, |
|
TogglePower, |
|
TogglePressure |
|
} |
|
string[] CommandTypeNames = Enum.GetNames (typeof (CommandType)); |
|
|
|
public sealed class Event { |
|
public EventType eventType; |
|
public long time; |
|
public CommandType commandType; |
|
public long targetId; |
|
|
|
public Event Copy () { |
|
return (Event) this.MemberwiseClone (); |
|
} |
|
public override string ToString () { |
|
return "[" + eventType + "," + time + "," + commandType + "," + targetId + "]"; |
|
} |
|
} |
|
|
|
public sealed class BlockState { |
|
public BlockState (IMyTerminalBlock block) { |
|
this.block = block; |
|
this.doorStatus = DoorStatus.Closed; |
|
this.sensorActive = false; |
|
UpdateState (); |
|
} |
|
public IMyTerminalBlock block; |
|
public DoorStatus doorStatus; |
|
public bool sensorActive; |
|
|
|
public EventType UpdateState () { |
|
if (this.block is IMyDoor) { |
|
IMyDoor door = this.block as IMyDoor; |
|
|
|
if (door.Status != this.doorStatus) { |
|
this.doorStatus = door.Status; |
|
switch (door.Status) { |
|
case DoorStatus.Open: |
|
return EventType.OnOpen; |
|
case DoorStatus.Opening: |
|
return EventType.OnOpening; |
|
case DoorStatus.Closed: |
|
return EventType.OnClose; |
|
case DoorStatus.Closing: |
|
return EventType.OnClosing; |
|
} |
|
} |
|
} else if (this.block is IMySensorBlock) { |
|
IMySensorBlock sensor = this.block as IMySensorBlock; |
|
if (sensor.IsActive != this.sensorActive) { |
|
this.sensorActive = sensor.IsActive; |
|
if (this.sensorActive) { |
|
return EventType.OnDetect; |
|
} |
|
return EventType.OnLost; |
|
} |
|
} |
|
return EventType.None; |
|
} |
|
|
|
public void DoOpen (bool? value) { |
|
if (this.block is IMyDoor) { |
|
IMyDoor door = this.block as IMyDoor; |
|
if (value == true) { |
|
door.OpenDoor (); |
|
} else if (value == false) { |
|
door.CloseDoor (); |
|
} else if (door.Status == DoorStatus.Closed || |
|
door.Status == DoorStatus.Closing) { |
|
door.OpenDoor (); |
|
} else { |
|
door.CloseDoor (); |
|
} |
|
} |
|
} |
|
|
|
public void DoPressurize (bool? value) { |
|
if (this.block is IMyAirVent) { |
|
IMyAirVent vent = this.block as IMyAirVent; |
|
if (value == true) { |
|
vent.Depressurize = false; |
|
} else if (value == false) { |
|
vent.Depressurize = true; |
|
} else { |
|
vent.Depressurize = !vent.Depressurize; |
|
} |
|
} |
|
} |
|
|
|
public void DoTogglePower (bool? value) { |
|
if (this.block is IMyFunctionalBlock) { |
|
IMyFunctionalBlock block = this.block as IMyFunctionalBlock; |
|
if (value == true) { |
|
block.Enabled = true; |
|
} else if (value == false) { |
|
block.Enabled = false; |
|
} else { |
|
block.Enabled = !block.Enabled; |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
List<string> displayBuffer = new List<String> (); |
|
Dictionary<long, Event[]> EventMap = new Dictionary<long, Event[]> (); |
|
Dictionary<long, BlockState> StateMap = new Dictionary<long, BlockState> (); |
|
List<Event> TimedEventQueue = new List<Event> (); |
|
|
|
public Program () { |
|
Runtime.UpdateFrequency = UpdateFrequency.Update10; |
|
DebugPrint ("Starting up...."); |
|
LoadBlocks (); |
|
} |
|
|
|
public void Main (string argument, UpdateType type) { |
|
if (type == UpdateType.Terminal || type == UpdateType.Trigger) { |
|
//reload blocks without restarting |
|
LoadBlocks (); |
|
} |
|
|
|
long time = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond; |
|
|
|
// check for state changes |
|
foreach (long id in EventMap.Keys) { |
|
BlockState state = StateMap[id]; |
|
EventType eventType = state.UpdateState (); |
|
if (eventType != EventType.None) { |
|
DebugPrint ("(" + id + ") " + eventType); |
|
foreach (Event e in EventMap[id]) { |
|
if (e.eventType == eventType) { |
|
Event timedEvent = e.Copy (); |
|
timedEvent.time += time; |
|
TimedEventQueue.Add (timedEvent); |
|
} |
|
} |
|
} |
|
} |
|
//fire event commands if time is right |
|
int index = 0; |
|
while (index < TimedEventQueue.Count) { |
|
Event e = TimedEventQueue[index]; |
|
if (e.time <= time) { |
|
DebugPrint ("Event: " + e); |
|
FireEvent (e.commandType, e.targetId); |
|
TimedEventQueue.RemoveAt (index); |
|
continue; |
|
} |
|
++index; |
|
} |
|
} |
|
|
|
public void FireEvent (CommandType cmd, long target) { |
|
switch (cmd) { |
|
case CommandType.Close: |
|
StateMap[target].DoOpen (false); |
|
break; |
|
case CommandType.Open: |
|
StateMap[target].DoOpen (true); |
|
break; |
|
case CommandType.ToggleDoor: |
|
StateMap[target].DoOpen (null); |
|
break; |
|
case CommandType.Pressurize: |
|
StateMap[target].DoPressurize (true); |
|
break; |
|
case CommandType.Depressurize: |
|
StateMap[target].DoPressurize (false); |
|
break; |
|
case CommandType.TogglePressure: |
|
StateMap[target].DoPressurize (null); |
|
break; |
|
case CommandType.TurnOn: |
|
StateMap[target].DoTogglePower (true); |
|
break; |
|
case CommandType.TurnOff: |
|
StateMap[target].DoTogglePower (false); |
|
break; |
|
case CommandType.TogglePower: |
|
StateMap[target].DoTogglePower (null); |
|
break; |
|
} |
|
} |
|
|
|
public void LoadBlocks () { |
|
DebugPrint ("Loading Block Data"); |
|
Dictionary<string, long> lookupMap = new Dictionary<string, long> (); |
|
EventMap.Clear (); |
|
|
|
//populate list |
|
List<IMyTerminalBlock> blockList = new List<IMyTerminalBlock> (); |
|
GridTerminalSystem.SearchBlocksOfName (BlockPrefix, blockList); |
|
|
|
foreach (IMyTerminalBlock block in blockList) { |
|
string name = block.CustomName; |
|
if (!name.StartsWith (BlockPrefix)) { |
|
continue; |
|
} |
|
long id = block.EntityId; |
|
name = name.Substring (BlockPrefix.Length).Trim ().ToLower (); |
|
if (name.Length > 0) { |
|
lookupMap[name] = id; |
|
} |
|
if (!StateMap.ContainsKey (id)) { |
|
StateMap.Add (id, new BlockState (block)); |
|
} |
|
EventMap[id] = null; |
|
} |
|
|
|
List<long> keys = new List<long> (EventMap.Keys); |
|
for (int index = 0; index < keys.Count; ++index) { |
|
long id = keys[index]; |
|
IMyTerminalBlock block = StateMap[id].block; |
|
DebugPrint ("Processing events for " + StateMap[id].block.CustomName); |
|
EventMap[id] = CreateBlockEventList (block, ref lookupMap); |
|
} |
|
} |
|
|
|
public Event[] CreateBlockEventList (IMyTerminalBlock block, ref Dictionary<string, long> lookupMap) { |
|
string data = block.CustomData; |
|
string[] lines = data.Split (new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); |
|
Event[] events = new Event[lines.Length]; |
|
//TODO handle invalid events better? |
|
for (int index = 0; index < lines.Length; ++index) { |
|
int line = index + 1; |
|
events[index] = new Event (); |
|
events[index].eventType = EventType.None; |
|
events[index].commandType = CommandType.None; |
|
events[index].targetId = block.EntityId; |
|
|
|
lines[index] = lines[index].Trim (); |
|
if (lines[index].Length == 0 || lines[index].StartsWith ("#")) { |
|
continue; |
|
} |
|
|
|
string[] tokens = lines[index].Split (new char[] { ' ' }, 4); |
|
//check token count or fail |
|
if (tokens.Length < 3) { |
|
DebugPrint ("Line " + line + ": too few parameters."); |
|
continue; |
|
|
|
} |
|
|
|
//parse time or fail |
|
float time = 0; |
|
|
|
if (!Single.TryParse (tokens[1], out time)) { |
|
events[index].eventType = EventType.None; |
|
DebugPrint ("Line " + line + ": Failed to parse time."); |
|
continue; |
|
} |
|
|
|
events[index].time = (long) (time * 1000); |
|
|
|
//parse event type or fail |
|
for (int j = 0; j < EventTypeNames.Length; ++j) { |
|
if (EventTypeNames[j].ToLower () == tokens[0].ToLower ()) { |
|
events[index].eventType = (EventType) j; |
|
break; |
|
} |
|
} |
|
if (events[index].eventType == EventType.None) { |
|
DebugPrint ("Line " + line + ": Invalid event type: " + tokens[0]); |
|
continue; |
|
} |
|
//parse command type of fail |
|
for (int j = 0; j < CommandTypeNames.Length; ++j) { |
|
if (CommandTypeNames[j].ToLower () == tokens[2].ToLower ()) { |
|
events[index].commandType = (CommandType) j; |
|
break; |
|
} |
|
} |
|
if (events[index].commandType == CommandType.None) { |
|
events[index].eventType = EventType.None; |
|
DebugPrint ("Line " + line + ": Invalid command type: " + tokens[2]); |
|
continue; |
|
} |
|
|
|
//lookup target block (if needed) or fail |
|
if (tokens.Length == 4) { |
|
string name = tokens[3].Trim ().ToLower (); |
|
if (!lookupMap.ContainsKey (name)) { |
|
events[index].eventType = EventType.None; |
|
DebugPrint ("Line " + line + ": Invalid target."); |
|
continue; |
|
} |
|
events[index].targetId = lookupMap[name]; |
|
} |
|
} |
|
return events; |
|
} |