Skip to content

Commit 1be127a

Browse files
committed
Version 0.4
1 parent ed78bee commit 1be127a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+6290
-11023
lines changed

.env.local

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ SO_API=http://localhost:8000
22
SO_MQTT=localhost
33
SO_MQTT_PORT=8083
44
SO_MQTT_PATH=/mqtt
5+
SO_MQTT_SSL=false
56
SO_MCS_USERNAME=sim
67
SO_MCS_PASSWORD=ops
78
SO_MCS_ADMIN_PASSWORD=admin
89
SO_MCS_SIMPLE=0
910
SO_CONTROL_TCP=tcp://localhost:5555
11+
SO_MINIO_ENDPOINT=localhost:9000

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ A Python (version 3.11 recommended) needs to be available, the list of required
4343

4444
**Note** A MQTT service, e.g. [Eclipse Mosquitto](https://mosquitto.org/) to run the scripts from a command line, check `containers/mqtt/config/mosquitto.conf` for an example configuration
4545

46+
**Note** A [MINIO](https://min.io/) instance is required for storing products objects, to run one using docker:
47+
48+
$ docker run -p 9000:9000 -p 9001:9001 -e "MINIO_ROOT_USER=username" -e "MINIO_ROOT_PASSWORD=password" quay.io/minio/minio server /data --console-address ":9001"
49+
4650
Once an MQTT is running and reachable and all the requirements installed, run:
4751

4852
# setup environment variables (required for all scripts and npm)

compose.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ services:
22
so-mqtt:
33
image: eclipse-mosquitto:latest
44
container_name: so-mqtt
5+
restart: unless-stopped
56
volumes:
67
- ./containers/mqtt/config:/mosquitto/config
78
ports:
89
- "8083:8083"
910
so-api:
1011
container_name: so-api
12+
restart: unless-stopped
1113
build:
1214
context: .
1315
dockerfile: ./containers/Dockerfile_so-api
@@ -19,8 +21,19 @@ services:
1921
- "8000:8000"
2022
environment:
2123
- SO_MCS_ADMIN_PASSWORD=${SO_MCS_ADMIN_PASSWORD}
24+
so-minio:
25+
container_name: so-minio
26+
restart: unless-stopped
27+
image: docker.io/bitnami/minio
28+
ports:
29+
- '9000:9000'
30+
- '9001:9001'
31+
environment:
32+
- MINIO_ROOT_USER=myusername
33+
- MINIO_ROOT_PASSWORD=mypassword
2234
so-master:
2335
container_name: so-master
36+
restart: unless-stopped
2437
build:
2538
context: .
2639
dockerfile: ./containers/Dockerfile_so-master
@@ -32,6 +45,7 @@ services:
3245
- so-mqtt
3346
so-mcs:
3447
container_name: so-mcs
48+
restart: unless-stopped
3549
build:
3650
context: .
3751
dockerfile: ./containers/Dockerfile_so-mcs
@@ -44,6 +58,7 @@ services:
4458
- SO_MQTT=${SO_MQTT}
4559
- SO_MQTT_PORT=${SO_MQTT_PORT}
4660
- SO_MQTT_PATH=${SO_MQTT_PATH}
61+
- SO_MQTT_SSL=${SO_MQTT_SSL}
4762
- SO_MCS_USERNAME=${SO_MCS_USERNAME}
4863
- SO_MCS_PASSWORD=${SO_MCS_PASSWORD}
4964
- SO_MCS_ADMIN_PASSWORD=${SO_MCS_ADMIN_PASSWORD}

containers/Dockerfile_so-master

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ RUN mkdir /app
1111
COPY sim-ops-lib/ /app
1212
WORKDIR /app
1313

14+
RUN python -c "import skyfield.api; skyfield.api.load('de421.bsp')"
15+
1416
ENTRYPOINT [ "python", "so-master.py" ]

containers/Dockerfile_so-mcs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
# build stage
3-
FROM node:20.2.0-bullseye-slim as build-stage
3+
FROM node:23 as build-stage
44

55
WORKDIR /app
66
COPY sim-ops-mcs/package*.json ./

containers/entrypoint.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ fi
2020
if [[ ! -z "${SO_MQTT_PATH}" ]]; then
2121
JSON_STRING="${JSON_STRING} 'SO_MQTT_PATH': '${SO_MQTT_PATH}',"
2222
fi
23+
if [[ ! -z "${SO_MQTT_SSL}" ]]; then
24+
JSON_STRING="${JSON_STRING} 'SO_MQTT_SSL': '${SO_MQTT_SSL}',"
25+
fi
2326
if [[ ! -z "${SO_MCS_SIMPLE}" ]]; then
2427
JSON_STRING="${JSON_STRING} 'SO_MCS_SIMPLE': parseInt('${SO_MCS_SIMPLE}'),"
2528
else

containers/mqtt/config/mosquitto.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
listener 1883
2-
listener 8083
2+
listener 8083 0.0.0.0
33
protocol websockets
44
allow_anonymous true
55

sim-ops-lib/data/scenarios/ops-sat-demo/data.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@
1111
"altitude": 127
1212
},
1313
"gs_initial_state": {
14-
"carrier_ul": "off"
14+
"program_track": true,
15+
"carrier_ul": "off",
16+
"power_ul": 50.0,
17+
"mode": "S_Sup_LBR"
1518
},
1619
"sc_initial_state": {
1720
"aocs_chain": "A",
18-
"pts_chain": "A",
21+
"eps_chain": "A",
22+
"eps_battery_dod": 50,
1923
"dhs_chain": "A",
24+
"dhs_memory": 100,
2025
"ttc_tx_status": "on",
2126
"ttc_chain": "A",
2227
"pl_gps_status": "off",
2328
"pl_camera_status": "off",
24-
"dhs_obsw_mode": "nominal"
29+
"dhs_obsw_mode": "nominal",
30+
"ttc_mode": "S_Sup_LBR",
31+
"ttc_obc": "nominal"
2532
}
2633
}

sim-ops-lib/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ scipy
1010
pytest
1111
zmq
1212
configparser
13+
minio
14+
minsp

sim-ops-lib/so-master.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import os, threading, time, json, logging, zmq, copy, glob
33
from datetime import datetime
44

5-
from so.core import load_scenario, Backend, OverrideState, Status
5+
from so.core import load_scenario, Backend, OverrideState, Status, ObjectStore, TTCState, Products, Quality
66
from so.ground_station import GroundStationSim
7-
from so.spacecraft import SpacecraftSim
7+
from so.spacecraft import SpacecraftSim, SpacePacketHandler
88

99
logger = logging.getLogger(__name__)
1010

@@ -13,6 +13,17 @@
1313
LAST_GS_STATE, LAST_SC_STATE = None, None
1414
CONTROL_HIST = []
1515
OV_STATE = OverrideState()
16+
SIM_UID = None
17+
18+
SO_GEN_PRODUCTS = bool(int(os.getenv('SO_GEN_PRODUCTS', 1)))
19+
if SO_GEN_PRODUCTS:
20+
OBJ_STORE = ObjectStore()
21+
SPH = SpacePacketHandler()
22+
PRODUCTS = Products(object_store=OBJ_STORE)
23+
else:
24+
OBJ_STORE = None
25+
SPH = None
26+
PRODUCTS = None
1627

1728
def sim_loop():
1829
logger.info('Starting sim loop')
@@ -29,31 +40,55 @@ def sim_loop():
2940
sc_state = SC_SIM.ping(ts, gs_state=gs_state, ov_state=OV_STATE)
3041
if sc_state:
3142
BACKEND.publish('spacecraft', sc_state.to_dict())
43+
# handle packet store
44+
if SO_GEN_PRODUCTS:
45+
# FIXME handle high priority tm edge case
46+
c1 = sc_state.status_dl == TTCState['FRAME_LOCK'] and sc_state.frame_quality == Quality.good and sc_state.frame_checks == Status.enabled and sc_state.ov_no_tm is not True
47+
c2 = sc_state.status_dl == TTCState['FRAME_LOCK'] and sc_state.frame_quality == Quality.good and sc_state.frame_checks == Status.disabled
48+
c3 = sc_state.status_dl == TTCState['FRAME_LOCK'] and sc_state.frame_quality != Quality.good and gs_state.frame_checks == Status.disabled
49+
if c1 or c2 or c3:
50+
# store packet every 5s
51+
if int(ts) % 5 == 0:
52+
# scrub packet data if only high priority is available
53+
if sc_state.ttc_obc == Status.error:
54+
OBJ_STORE.store(SIM_UID+'-tm', str(int(ts)), SPH.space_packet(sc_state.scrub()).as_bytes())
55+
else:
56+
OBJ_STORE.store(SIM_UID+'-tm', str(int(ts)), SPH.space_packet(sc_state).as_bytes())
3257

3358
end = time.time()
3459
delta = SCENARIO.time_step - (end - start)
3560

3661
ts += delta
62+
if delta <= 0:
63+
delta = 1
64+
3765
time.sleep(delta)
3866

3967
def start_sim(uid):
4068
logger.info(f"Starting sim { uid }")
4169

42-
global SCENARIO, GS_SIM, SC_SIM, BACKEND, END_SIM, LAST_GS_STATE, LAST_SC_STATE
70+
global SCENARIO, GS_SIM, SC_SIM, BACKEND, END_SIM, LAST_GS_STATE, LAST_SC_STATE, SIM_UID
4371
END_SIM = threading.Event()
4472

4573
SCENARIO = load_scenario(uid)
4674
GS_SIM = GroundStationSim(SCENARIO, initial_state=LAST_GS_STATE)
4775
SC_SIM = SpacecraftSim(SCENARIO, initial_state=LAST_SC_STATE)
4876
BACKEND = Backend(SCENARIO)
4977

78+
SIM_UID = 'sim-' + datetime.utcnow().isoformat(sep='T', timespec='minutes').replace('-','.').replace(':','h').replace('T', '-')
79+
logger.info(f"Sim UID set to: { SIM_UID }")
80+
81+
if SO_GEN_PRODUCTS:
82+
logger.info('Products handling: flight dynamics start sim')
83+
PRODUCTS.fd_start_sim()
84+
5085
t = threading.Thread(target=sim_loop)
5186
t.start()
5287

5388
def stop_sim():
5489
logger.info(f"Stopping sim")
5590

56-
global END_SIM, GS_SIM, SC_SIM, LAST_GS_STATE, LAST_SC_STATE, CONTROL_HIST
91+
global END_SIM, GS_SIM, SC_SIM, LAST_GS_STATE, LAST_SC_STATE, CONTROL_HIST, SIM_UID
5792
if END_SIM == None or SCENARIO == None or GS_SIM == None or SC_SIM == None:
5893
logger.info("No sim running")
5994
return
@@ -63,6 +98,10 @@ def stop_sim():
6398
LAST_GS_STATE = copy.deepcopy(GS_SIM.state) if GS_SIM else None
6499
LAST_SC_STATE = copy.deepcopy(SC_SIM.state) if SC_SIM else None
65100

101+
if SO_GEN_PRODUCTS:
102+
logger.info('Products handling: flight dynamics stop sim')
103+
PRODUCTS.fd_stop_sim(SIM_UID, LAST_GS_STATE)
104+
66105
# save control history to file and reset
67106
_path, _now = os.path.join('data', 'hist'), int(datetime.utcnow().timestamp())
68107
if not os.path.exists(_path):
@@ -71,6 +110,8 @@ def stop_sim():
71110
json.dump(CONTROL_HIST, fout)
72111
CONTROL_HIST = []
73112

113+
SIM_UID = None
114+
74115
def _log_control(data, result, ts):
75116
global CONTROL_HIST
76117

@@ -124,21 +165,24 @@ def control_loop():
124165

125166
_ok, _fail = { 'status': 'OK' }, { 'status': 'FAIL'}
126167
_admin = False
127-
if 'admin' in data:
128-
_admin = data['admin']
168+
if 'admin' in data and data['admin'] is True:
169+
_admin = True
170+
else:
171+
_admin = False
129172

130173
# handle spacecraft controls
131-
if 'system' in data and data['system'] == 'spacecraft':
174+
if 'system' in data and data['system'] == 'spacecraft' and _admin is False:
132175
_fail = { 'status': 'r:REL r:ACC r:FAIL'}
133176

134-
# need at least ground station U/L carrier enabled to send command
135-
if _admin is False and (GS_SIM is None or GS_SIM.state.carrier_ul is None or GS_SIM.state.carrier_ul == Status.off):
177+
# need at least ground station U/L carrier enabled and sweep done to send commands
178+
if GS_SIM is None or GS_SIM.state.carrier_ul is None or GS_SIM.state.carrier_ul == Status.off or GS_SIM.state.sweep_done is False:
136179
result = { 'status': 'r:REL r:ACC r:FAIL'}
137180
_ts = GS_SIM.state.ts
138181
elif GS_SIM.state.power_ul < 50:
139182
result = { 'status': 'g:REL r:ACC r:FAIL'}
140183
_ts = GS_SIM.state.ts
141184
else:
185+
print('try', data)
142186
try:
143187
if OV_STATE.no_tc is not True:
144188
result = SC_SIM.control(data, ov_state=OV_STATE, admin=_admin)
@@ -184,6 +228,14 @@ def control_loop():
184228
case other:
185229
result = _fail
186230

231+
# handle admin control for spacecraft, without overrides and requiring TC
232+
elif 'system' in data and data['system'] == 'spacecraft' and _admin is True:
233+
result = SC_SIM.control(data, admin=_admin)
234+
if 'OK' in result['status']:
235+
result = _ok
236+
else:
237+
result = _fail
238+
187239
# handle overrides
188240
elif 'system' in data and data['system'] == 'override':
189241
if data['value'] is None or len(data['value']) == 0:

0 commit comments

Comments
 (0)