diff --git a/.gitignore b/.gitignore
index 66fd13c..f04ddec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,7 @@
# Dependency directories (remove the comment below to include it)
# vendor/
+
+# Ignore environment files
+.env
+.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
index 00f6140..c8dccee 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,102 @@
-# deveui-cli
\ No newline at end of file
+
+
+
+
+
+
+
DevEUI CLI
+
+
+
A Golang program for concurrently registering DevEUI identifiers for MachineMax.
+
+
+
+## About MachineMax DevEUI
+
+Each MachineMax sensor has a unique 16-character (hex) identifier called a DevEUI. As part of
+the manufacturing process, it is written onto the internal storage of the sensor. The DevEUI is
+also printed on a label on the side of the sensor alongside a 5-character code (the last 5
+characters of the DevEUI). For example, a DevEUI of 78111FFFE452555B would have a short
+code of 2555B.
+
+The sensors communicate with the MachineMax cloud though a LoRaWAN provider and the
+LoRaWAN provider uses the DevEUI to identify the sensor. This means we first have to register
+the DevEUI with the provider before we can use it. We pay for every device registered with the
+LoRaWAN provider, so it is important that we only register DevEUIs that we use.
+
+When a customer registers a new sensor, they will enter the 5-character short-form code instead
+of the full DevEUI, so it is essential that each DevEUI in the batch has a unique 5-char code (for
+lookups).
+
+### Built With
+
+* Golang
+* Docker
+
+## Getting Started
+
+To run this code, we first need a `.env` file in the root of the project. Once this is done, add these vars:
+
+```
+BASE_URL=http://europe-west1-machinemax-dev-d524.cloudfunctions.net
+TIMEOUT=30000
+CODE_REGISTRATION_LIMIT=100
+```
+
+### Prerequisites
+
+- You will need Golang to run this program which can be downloaded at: [https://go.dev/](https://go.dev/)
+- This program can also be run using docker, this can be downloaded at: [https://www.docker.com/](https://www.docker.com/)
+
+## Usage
+
+Then to run locally, use: `go run main.go`.
+
+Alternatively, this code can also be run via docker. To build the docker image use:
+```
+docker build -t deveui-cli . --build-arg BASE_URL=${BASE_URL} --build-arg TIMEOUT=${TIMEOUT} --build-arg CODE_REGISTRATION_LIMIT=${CODE_REGISTRATION_LIMIT}.
+```
+
+To run the docker image:
+```
+docker run deveui-cli
+```
+
+To run the tests use:
+```
+go test ./...
+```
+To check code coverage:
+```
+go test -coverprofile=coverage.out ./... ; go tool cover -html=coverage.out
+```
+
+And to run benchmark tests for CPU and memory consumption:
+```
+go test -bench=. -benchmem
+```
+
+Finally to check for race conditions, use:
+```
+go test -race ./...
+```
+
+## Checklist
+
+- [x] Implement solution
+- [x] Write unit tests
+- [x] Write readme
+- [ ] More unit tests around unhappy paths for higher code coverage
+- [ ] Discuss with engineers how to improve code
+
+## Contact
+
+Email - nickgowdy87@gmail.com
+
+Website http://www.nickgowdy.com/
+
+Github https://github.com/nickgowdy
+
+
+
+
diff --git a/client/lorawan.go b/client/lorawan.go
new file mode 100644
index 0000000..d832e17
--- /dev/null
+++ b/client/lorawan.go
@@ -0,0 +1,67 @@
+package client
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "net/http"
+
+ "github.com/NickGowdy/deveui-cli/device"
+)
+
+// Client used to communicate to external services
+type Client interface {
+ Do(*http.Request) (resp *http.Response, err error)
+}
+
+// LoraWAN used to communicate to LoRaWAN external system
+type LoraWAN struct {
+ baseURL string
+ client Client
+}
+
+func NewLoraWAN(baseURL string, client Client) *LoraWAN {
+ return &LoraWAN{
+ baseURL: baseURL,
+ client: client,
+ }
+}
+
+const endpoint = "/sensor-onboarding-sample" // endpoint for saving DevEUI via LoRaWAN
+
+// RegisterDevice registers new device using LoraWAN external service
+func (l *LoraWAN) RegisterDevice(ctx context.Context) (*device.Device, error) {
+ device := device.NewDevice()
+ identifier := device.GetIdentifier()
+ b := new(bytes.Buffer)
+ reqBody := map[string]string{"Deveui": identifier}
+
+ err := json.NewEncoder(b).Encode(&reqBody)
+ if err != nil {
+ return nil, err
+ }
+
+ fullUrl := l.baseURL + endpoint
+ req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, b)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := l.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusOK {
+ return device, nil
+ } else {
+ return nil, errors.New(resp.Status)
+ }
+}
diff --git a/client/lorawan_test.go b/client/lorawan_test.go
new file mode 100644
index 0000000..e10ddf3
--- /dev/null
+++ b/client/lorawan_test.go
@@ -0,0 +1,95 @@
+package client
+
+import (
+ "net/http"
+)
+
+type MockClient struct {
+ DoFunc func(*http.Request) (resp *http.Response, err error)
+}
+
+// func TestLorawanClientHappyPath(t *testing.T) {
+// mockClient := &MockClient{
+// DoFunc: func(*http.Request) (resp *http.Response, err error) {
+// return &http.Response{}, nil
+// },
+// }
+
+// loraWAN := NewLoraWAN("www.example.com", mockClient)
+
+// b := new(bytes.Buffer)
+// reqBody := map[string]string{"Deveui": "Abcde"}
+
+// _ = json.NewEncoder(b).Encode(&reqBody)
+
+// ctx, cancel := context.WithCancel(context.Background())
+
+// if cancel == nil {
+// t.Errorf("cancel should not be nil but is: %v", cancel)
+// }
+
+// resp, err := loraWAN.DoPost(b, ctx)
+
+// if err != nil {
+// t.Errorf("err should be nil but is: %s", err.Error())
+// }
+// defer resp.Body.Close()
+
+// if resp.StatusCode != 200 {
+// t.Errorf("resp should be nil but is: %d", resp.StatusCode)
+// }
+
+// body, _ := io.ReadAll(resp.Body)
+// val := string(body)
+
+// if strings.TrimSpace(val) != "true" {
+// t.Errorf("body should equal true but is: %d", body)
+// }
+// }
+
+// func TestNewLoraWanClient(t *testing.T) {
+// client := &http.Client{
+// Timeout: 30 * time.Second,
+// }
+// t.Parallel()
+// type args struct {
+// timeout time.Duration
+// }
+// tests := []struct {
+// name string
+// args args
+// want *LoraWAN
+// }{
+// {
+// name: "create-new-lorawan-client",
+// args: args{
+// timeout: 30,
+// },
+// want: &LoraWAN{
+// baseURL: "https://www.example.com",
+// client: client,
+// },
+// },
+// }
+// for _, tt := range tests {
+// tt := tt // it is important to capture range variable
+// t.Run(tt.name, func(t *testing.T) {
+// t.Parallel() // this makes sure that all cases from the table here are executed in parallel
+// if got := NewLoraWAN("https://www.example.com", client); !reflect.DeepEqual(got, tt.want) {
+// t.Errorf("NewLoraWanClient() = %v, want %v", got, tt.want)
+// }
+// })
+// }
+// }
+
+// func (m *MockClient) Do(*http.Request) (resp *http.Response, err error) {
+// b := new(bytes.Buffer)
+// reqBody := true
+
+// _ = json.NewEncoder(b).Encode(&reqBody)
+// return &http.Response{
+// StatusCode: http.StatusOK,
+// Body: io.NopCloser(b),
+// Status: "200 OK"},
+// nil
+// }
diff --git a/device/device.go b/device/device.go
new file mode 100644
index 0000000..6e94089
--- /dev/null
+++ b/device/device.go
@@ -0,0 +1,65 @@
+package device
+
+import (
+ "crypto/rand"
+ "fmt"
+ "log"
+ "math/big"
+)
+
+const (
+ AllowedChars = "ABCDEF0123456789" // accepted chars used to make up DevEUI
+ DevEuiLength = 16 // valid DevEUI is string of length 16
+)
+
+type Device struct {
+ identifier string
+ code string
+}
+
+// NewDevice Build a new device with DevEUI identifier and code values.
+//
+// # Example
+//
+// 1CEB0080F074F750 4F750
+func NewDevice() *Device {
+ hex, err := generateHexString()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return &Device{
+ identifier: hex,
+ code: hex[len(hex)-5:],
+ }
+}
+
+func (d Device) GetIdentifier() string {
+ return d.identifier
+}
+
+func (d Device) GetCode() string {
+ return d.code
+}
+
+func (d Device) Print() {
+ fmt.Printf("device has identifier: %s and code: %s\n", d.identifier, d.code)
+}
+
+// Generate valid DevEUI identifier value.
+//
+// # Example
+//
+// 1CEB0080F074F750
+func generateHexString() (string, error) {
+ max := big.NewInt(int64(len(AllowedChars)))
+ b := make([]byte, DevEuiLength)
+ for i := range b {
+ n, err := rand.Int(rand.Reader, max)
+ if err != nil {
+ return "", err
+ }
+ b[i] = AllowedChars[n.Int64()]
+ }
+ return string(b), nil
+}
diff --git a/device/device_test.go b/device/device_test.go
new file mode 100644
index 0000000..46fa331
--- /dev/null
+++ b/device/device_test.go
@@ -0,0 +1,90 @@
+package device
+
+import (
+ "testing"
+)
+
+func TestCanGenerateValidCode(t *testing.T) {
+ allowedChars := []string{"A", "B", "C", "D", "E", "F", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
+ device := NewDevice()
+
+ if device == nil {
+ t.Errorf("deivce should not be nil, but is %s", device)
+ }
+
+ identifier := device.GetIdentifier()
+ code := device.GetCode()
+
+ if len(code) != 5 {
+ t.Errorf("code should be 5 characters long, but is %d", len(code))
+ }
+
+ if identifier[len(identifier)-5:] != code {
+ t.Errorf("code should be last 5 characters of identifier, but is %s", code)
+ }
+
+ hasChar := false
+ for _, char := range allowedChars {
+ if char == string(code[0]) {
+ hasChar = true
+ } else if hasChar {
+ break
+ }
+ }
+
+ if hasChar == false {
+ t.Errorf("first char should be A, B, C, D, E, F, 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9: but is: %s", string(code[0]))
+ }
+
+ hasChar = false
+ for _, char := range allowedChars {
+ if char == string(code[1]) {
+ hasChar = true
+ } else if hasChar {
+ break
+ }
+ }
+
+ if hasChar == false {
+ t.Errorf("second char should be A, B, C, D, E, F, 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9: but is: %s", string(code[1]))
+ }
+
+ hasChar = false
+ for _, char := range allowedChars {
+ if char == string(code[2]) {
+ hasChar = true
+ } else if hasChar {
+ break
+ }
+ }
+
+ if hasChar == false {
+ t.Errorf("third char should be A, B, C, D, E, F, 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9: but is: %s", string(code[2]))
+ }
+
+ hasChar = false
+ for _, char := range allowedChars {
+ if char == string(code[3]) {
+ hasChar = true
+ } else if hasChar {
+ break
+ }
+ }
+
+ if hasChar == false {
+ t.Errorf("fourth char should be A, B, C, D, E, F, 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9: but is: %s", string(code[3]))
+ }
+
+ hasChar = false
+ for _, char := range allowedChars {
+ if char == string(code[4]) {
+ hasChar = true
+ } else if hasChar {
+ break
+ }
+ }
+
+ if hasChar == false {
+ t.Errorf("fifth char should be A, B, C, D, E, F, 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9: but is: %s", string(code[4]))
+ }
+}
diff --git a/dockerfile b/dockerfile
new file mode 100644
index 0000000..467cc91
--- /dev/null
+++ b/dockerfile
@@ -0,0 +1,20 @@
+FROM golang:1.19-alpine
+
+RUN apk add --no-cache git
+
+# Set the Current Working Directory inside the container
+WORKDIR /app/deveui-cli
+
+# We want to populate the module cache based on the go.{mod,sum} files.
+COPY go.mod .
+
+RUN go mod download
+
+COPY . .
+
+# Build the Go app
+RUN go build -o ./deveui-cli .
+
+
+# Run the binary program produced by `go install`
+ENTRYPOINT ["./deveui-cli"]
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..aa4b41a
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/NickGowdy/deveui-cli
+
+go 1.19
+
+require github.com/joho/godotenv v1.5.1 // direct
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d61b19e
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
diff --git a/images/logo.jpeg b/images/logo.jpeg
new file mode 100644
index 0000000..200b7a6
Binary files /dev/null and b/images/logo.jpeg differ
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..c9bccb2
--- /dev/null
+++ b/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/NickGowdy/deveui-cli/client"
+ "github.com/NickGowdy/deveui-cli/processor"
+ "github.com/joho/godotenv"
+)
+
+/*
+deveui-cli is a Go CLI program.
+It is used for concurrently generating unique 16-character (hex) identifier called a DevEUI.
+These are generated by the program and registered via an external (LoRaWAN) API.
+
+Usage:
+
+ go run main.go (locally)
+
+ go run deveui-cli (docker)
+
+Once this program starts, it will listen to syscall.SIGTERM, syscall.SIGINT via a channel.
+This is to handle any unexpected terminations of the program and to resume processing DevEUIs.
+*/
+func main() {
+ if err := godotenv.Load(".env"); err != nil {
+ panic("error loading.env file")
+ }
+
+ baseurl := os.Getenv("BASE_URL")
+
+ maxConcurrentJobs, err := strconv.Atoi(os.Getenv("MAX_CONCURRENT_JOBS"))
+ if err != nil {
+ panic("error parsing MAX_CONCURRENT_JOBS to int")
+ }
+
+ codeRegistrationLimit, err := strconv.Atoi(os.Getenv("CODE_REGISTRATION_LIMIT"))
+ if err != nil {
+ panic("error parsing CODE_REGISTRATION_LIMIT to int")
+ }
+
+ timeout, err := strconv.Atoi(os.Getenv("TIMEOUT"))
+ if err != nil {
+ panic("error parsing TIMEOUT to int")
+ }
+
+ // setup client for requests
+ httpClient := &http.Client{
+ Timeout: time.Second * time.Duration(timeout),
+ }
+ loraWAN := client.NewLoraWAN(baseurl, httpClient)
+
+ // setup processor to do work
+ processor := &processor.Processor{
+ CodeRegistrationLimit: codeRegistrationLimit,
+ MaxConcurrentJobs: maxConcurrentJobs,
+ LoraWAN: *loraWAN,
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ processor.Start(ctx, cancel)
+}
diff --git a/main_test.go b/main_test.go
new file mode 100644
index 0000000..88cec9f
--- /dev/null
+++ b/main_test.go
@@ -0,0 +1,7 @@
+package main
+
+import "testing"
+
+func BenchmarkMain(b *testing.B) {
+ main()
+}
diff --git a/processor/code_processor.go b/processor/code_processor.go
new file mode 100644
index 0000000..143c881
--- /dev/null
+++ b/processor/code_processor.go
@@ -0,0 +1,58 @@
+package processor
+
+import (
+ "context"
+ "log"
+
+ "github.com/NickGowdy/deveui-cli/client"
+)
+
+type Processor struct {
+ CodeRegistrationLimit int
+ MaxConcurrentJobs int
+ LoraWAN client.LoraWAN
+}
+
+func (p *Processor) Start(ctx context.Context, cancel context.CancelFunc) {
+ // workCh := make(chan struct{})
+ count := 0
+
+ for count < p.CodeRegistrationLimit {
+ device, err := p.LoraWAN.RegisterDevice(ctx)
+ if err != nil {
+ log.Print(err)
+ } else {
+ device.Print()
+ count++
+ }
+ }
+
+ // go func(ctx context.Context) {
+ // for {
+ // p.doWork(ctx, workCh)
+ // }
+ // }(ctx)
+
+ // for {
+ // select {
+ // case <-ctx.Done():
+ // return
+ // case <-workCh:
+ // count++
+ // if count == p.CodeRegistrationLimit {
+ // cancel()
+ // fmt.Printf("work complete \n")
+ // }
+ // }
+ // }
+}
+
+// func (cp *Processor) doWork(ctx context.Context, workCh chan<- struct{}) {
+// device, err := cp.LoraWAN.RegisterDevice(ctx)
+// if err != nil {
+// return
+// } else {
+// device.Print()
+// workCh <- struct{}{}
+// }
+// }
diff --git a/processor/code_processor_test.go b/processor/code_processor_test.go
new file mode 100644
index 0000000..84334f2
--- /dev/null
+++ b/processor/code_processor_test.go
@@ -0,0 +1,87 @@
+package processor
+
+import (
+ "net/http"
+)
+
+type MockClient struct {
+ DoFunc func(*http.Request) (resp *http.Response, err error)
+}
+
+const (
+ MAX_CONCURRENT_JOBS = 2
+ CODE_REGISTRATION_LIMIT = 10
+)
+
+// func TestCanProcessCodes(t *testing.T) {
+// mockClient := &MockClient{
+// DoFunc: func(*http.Request) (resp *http.Response, err error) {
+// return &http.Response{}, nil
+// },
+// }
+
+// loraWAN := client.NewLoraWAN("www.example.com", mockClient)
+
+// codeProcessor := &CodeProcessor{
+// CodeRegistrationLimit: CODE_REGISTRATION_LIMIT,
+// MaxConcurrentJobs: MAX_CONCURRENT_JOBS,
+// DeviceCh: make(chan device.Device),
+// LoraWAN: *loraWAN,
+// }
+
+// ctx, cancel := context.WithCancel(context.Background())
+// defer cancel()
+
+// work := make(chan struct{}, MAX_CONCURRENT_JOBS)
+
+// go func() {
+// for {
+// work <- struct{}{}
+// }
+// }()
+
+// // Spawn workers
+// for j := 0; j < MAX_CONCURRENT_JOBS; j++ {
+// go codeProcessor.Worker(ctx, work)
+// }
+
+// n := 0
+// for d := range codeProcessor.DeviceCh {
+
+// identifier := d.GetIdentifier()
+// code := d.GetCode()
+
+// if code == "" {
+// t.Error("code should not be nil")
+// }
+
+// if identifier == "" {
+// t.Error("identifier should not be nil")
+// }
+
+// if identifier[len(identifier)-5:] != code {
+// t.Errorf("code should be last 5 characters of identifier, but is %s", code)
+// }
+
+// if len(identifier) != 16 {
+// t.Errorf("identifier should be exactly 16 characters, but is %d", len(identifier))
+// }
+
+// if len(code) != 5 {
+// t.Errorf("code should be exactly 5 characters, but is %d", len(code))
+// }
+
+// n += 1
+// if n == CODE_REGISTRATION_LIMIT {
+// break
+// }
+// }
+// }
+
+// func (m *MockClient) Do(*http.Request) (resp *http.Response, err error) {
+// return &http.Response{
+// StatusCode: http.StatusOK,
+// Body: io.NopCloser(bytes.NewReader(nil)),
+// Status: "200 OK"},
+// nil
+// }