Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/pod
*.toml
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ Fake pod implementation

* This version also mimics a behavior we see in some DASH pods where the pod disconnects every 3 minutes; this can be used with iOS hooks to make a heartbeat to run Loop in situations where a BLE CGM is not available.

* It also has a websocket based API that can used by a separate [NodeJS/React frontend](https://github.com/ps2/pod_simulator_frontend), that is installed and run separately for now.
* It serves a control panel at http://<host>:8080/ (`-api` changes the address): the pod's reservoir, delivery, age, alerts and fault, with controls to set the reservoir and pod age, raise alerts, trigger or clear a fault, and crash on the next command to test unacknowledged commands. The panel drives a JSON API: `GET /status`, and `POST /command` with `{"command": "changeReservoir", "value": 45}` (also `setActiveTime` in minutes, `setAlerts` as a slot mask, `setFault` with a fault code, 0 to clear, and `crashNextCommand` with `"beforeProcessing": true|false`).

* The websocket API at `/ws`, used by the separate [NodeJS/React frontend](https://github.com/ps2/pod_simulator_frontend), is still there.

* It can serve an app in the iOS Simulator instead of a phone; see [Run against the iOS Simulator](#run-against-the-ios-simulator).

Requirements:
1. Version of iOS code (Loop app) that will interact with this simulator - Loop dev branch or FreeAPS freeaps_dev branch
Expand Down Expand Up @@ -71,6 +75,33 @@ FATA[####] pkg bluetooth; ** disconnect:

Simple restore communication as stated above.

## Run against the iOS Simulator

`-transport impossible` serves the pod to an app in the iOS Simulator through
[ImpossiBLE](https://github.com/mickeyl/ImpossiBLE): no Raspberry Pi and no radio, so it runs on any Mac. The app is
launched with ImpossiBLE's library injected, as [LoopKit/g7sim](https://github.com/LoopKit/g7sim) describes for the G7
emulator (its `simulator/build-impossible.sh` builds the library), and talks to the pod over ImpossiBLE's socket:

```
go build
./pod -transport impossible -fresh
```

Loop's Omnipod support (OmnipodKit) mocks pod commands in the Simulator unless it finds ImpossiBLE, and then pairs,
primes and runs the simulated pod as it would a real one.

ImpossiBLE connects the app to one socket, `/tmp/impossible.sock`. To run the pod alongside g7sim for the same app, give
each its own socket and let g7sim's `impossible-mux` hold ImpossiBLE's:

```
./pod -transport impossible -socket /tmp/impossible-pod.sock
g7sim -transport impossible -socket /tmp/impossible-g7.sock
impossible-mux /tmp/impossible-g7.sock /tmp/impossible-pod.sock
```

The pod holds a connect that asks for a start delay (`CBConnectPeripheralOptionStartDelayKey`) for that delay, as iOS
does, and hangs up after 3 minutes without a command, as above.

# Original README.md

We maintained the original README file below. It may be helpful if someone plans to cross-compile the code and just transfer the executable.
Expand Down
40 changes: 26 additions & 14 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"flag"
"time"

"github.com/avereha/pod/pkg/api"
"github.com/avereha/pod/pkg/bluetooth"
Expand All @@ -18,6 +17,9 @@ func main() {
// if both verbose and quiet are chosen, e.g., -v -q, the verbose dominates
var traceLevel = flag.Bool("v", false, "verbose off by default, TraceLevel")
var infoLevel = flag.Bool("q", false, "quiet off by default, InfoLevel")
var apiAddr = flag.String("api", ":8080", "address of the control panel and its API")
var transport = flag.String("transport", "ble", "ble (a Linux Bluetooth adapter) or impossible (ImpossiBLE's socket, for an app in the iOS Simulator)")
var socket = flag.String("socket", "/tmp/impossible.sock", "ImpossiBLE socket for -transport impossible")

flag.Parse()

Expand All @@ -35,34 +37,44 @@ func main() {
FullTimestamp: true,
})

// TODO: This is kinda ugly, move state reader into own file and pass state to both BLE and pod
state := &pod.PODState{
Filename: *stateFile,
}
var err error
if !(*freshState) {
var state *pod.PODState
if *freshState {
state = pod.NewFreshState(*stateFile)
} else {
var err error
state, err = pod.NewState(*stateFile)
if err != nil {
log.Fatalf("pkg pod; could not restore pod state from %s: %+v", *stateFile, err)
}
}
if state.PeripheralID == "" {
state.PeripheralID = bluetooth.PeripheralID()
}

log.Tracef("podId %x", state.Id)

ble, err := bluetooth.New("hci0", state.Id)
//defer ble.Close()
var attach func(*bluetooth.Ble) (bluetooth.Radio, error)
switch *transport {
case "ble":
attach = func(b *bluetooth.Ble) (bluetooth.Radio, error) { return bluetooth.NewGattRadio(b, state.Id) }
case "impossible":
attach = func(b *bluetooth.Ble) (bluetooth.Radio, error) {
return bluetooth.NewSimRadio(b, *socket, state.PeripheralID, state.Id)
}
default:
log.Fatalf("transport %q: want ble or impossible", *transport)
}
ble, err := bluetooth.New(attach)
if err != nil {
log.Fatalf("Could not start BLE: %s", err)
log.Fatalf("Could not start %s: %s", *transport, err)
}

p := pod.New(ble, *stateFile, *freshState)
p := pod.New(ble, state)
go func() {
p.StartAcceptingCommands()
}()

log.Info("Starting API")
s := api.New(p)
s.Start()

time.Sleep(9999 * time.Second)
s.Start(*apiAddr)
}
110 changes: 78 additions & 32 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package api

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/avereha/pod/pkg/pod"
"github.com/gorilla/websocket"
Expand All @@ -26,18 +29,23 @@ func New(pod *pod.Pod) *Server {
return ret
}

func (s *Server) Start() {
fmt.Println("Pod simulator web api listening on :8080")
func (s *Server) Start(addr string) {
s.setupRoutes()
fmt.Println("Setting Web Message Hook")
s.pod.SetWebMessageHook(func(msg []byte) {
s.sendMessage(msg)
})
http.ListenAndServe(":8080", nil)
log.Infof("Pod simulator control panel on http://%s/", displayAddr(addr))
log.Fatal(http.ListenAndServe(addr, nil))
}

func displayAddr(addr string) string {
if strings.HasPrefix(addr, ":") {
return "localhost" + addr
}
return addr
}

func (s *Server) sendMessage(msg []byte) {
fmt.Println("writing to websocket")
if s.conn != nil {
if err := s.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
log.Println(err)
Expand All @@ -48,19 +56,48 @@ func (s *Server) sendMessage(msg []byte) {

func (s *Server) setupRoutes() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is an API to the pod simulator intended to be used with a separate web client.")
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, indexHTML)
})
http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, s.pod.Status())
})
http.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST a command", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err == nil {
err = s.handleCommand(body)
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, s.pod.Status())
})
// The websocket API, for github.com/ps2/pod_simulator_frontend.
http.Handle("/ws", s)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Host)
func writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(v)
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// upgrade this connection to a WebSocket
// connection
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}

s.conn = ws
Expand All @@ -87,56 +124,65 @@ func (s *Server) reader(conn *websocket.Conn) {
log.Println(err)
return
}
fmt.Println("Received: " + string(p))
s.handleCommand(p)
log.Debugf("Received: %s", p)
if err := s.handleCommand(p); err != nil {
log.Error(err)
}
}
}

func (s *Server) handleCommand(bytes []byte) {
func (s *Server) handleCommand(bytes []byte) error {
var msg map[string]interface{}
if err := json.Unmarshal(bytes, &msg); err != nil {
log.Error(err)
return
return err
}

var (
command string
value float64
ok bool
)

if command, ok = msg["command"].(string); !ok {
log.Fatal("command is not a string or not in msg")
command, ok := msg["command"].(string)
if !ok {
return errors.New("command is not a string or not in msg")
}
value, hasValue := msg["value"].(float64)
needValue := func(what string, min, max float64) error {
if !hasValue {
return fmt.Errorf("%s is not a number or not in msg", what)
}
if value < min || value > max {
return fmt.Errorf("%s %v: want %v to %v", what, value, min, max)
}
return nil
}

switch command {
case "changeReservoir":
if value, ok = msg["value"].(float64); !ok {
log.Fatal("reservoir value is not a number or not in msg")
if err := needValue("reservoir", 0, 200); err != nil {
return err
}
s.pod.SetReservoir(float32(value))
case "setAlerts":
if value, ok = msg["value"].(float64); !ok {
log.Fatal("alert value is not a number or not in msg")
if err := needValue("alert mask", 0, 255); err != nil {
return err
}
s.pod.SetAlerts(uint8(value))
case "setFault":
if value, ok = msg["value"].(float64); !ok {
log.Fatal("fault value is not a number or not in msg")
if err := needValue("fault", 0, 255); err != nil {
return err
}
s.pod.SetFault(uint8(value))
case "setActiveTime":
if value, ok = msg["value"].(float64); !ok {
log.Fatal("active time in minutes is not a number or not in msg")
if err := needValue("active time in minutes", 0, 100*60); err != nil {
return err
}
s.pod.SetActiveTime(int(value))
case "crashNextCommand":
var beforeProcessing bool
if beforeProcessing, ok = msg["beforeProcessing"].(bool); !ok {
log.Fatal("beforeProcessing is not a bool or not in msg")
beforeProcessing, ok := msg["beforeProcessing"].(bool)
if !ok {
return errors.New("beforeProcessing is not a bool or not in msg")
}
s.pod.CrashNextCommand(beforeProcessing)
default:
return fmt.Errorf("unknown command %q", command)
}
return nil
}

// We'll need to define an Upgrader
Expand Down
Loading
Loading